diff --git a/netbox/extras/api/serializers.py b/netbox/extras/api/serializers.py index 94d2e32cc..f996876b9 100644 --- a/netbox/extras/api/serializers.py +++ b/netbox/extras/api/serializers.py @@ -11,7 +11,7 @@ from dcim.api.nested_serializers import ( from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup from extras.choices import * from extras.models import * -from extras.models.staging import Notification +from extras.models.staging import Notification, ReviewRequest from extras.utils import FeatureQuery from netbox.api.exceptions import SerializerNotFound from netbox.api.fields import ChoiceField, ContentTypeField, SerializedPKRelatedField @@ -537,3 +537,16 @@ class NotificationSerializer(BaseModelSerializer): fields = [ 'id', 'url', 'created', 'title', 'content', 'read' ] + + +class ReviewRequestSerializer(BaseModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='extras-api:review-requests-detail') + + def create(self, validated_data): + pass + + class Meta: + model = ReviewRequest + fields = [ + 'id', 'url', 'created', 'last_updated', 'owner', 'reviewer', 'branch', 'status', 'state' + ] diff --git a/netbox/extras/api/urls.py b/netbox/extras/api/urls.py index 71f829242..1bca3d11f 100644 --- a/netbox/extras/api/urls.py +++ b/netbox/extras/api/urls.py @@ -20,6 +20,7 @@ router.register('object-changes', views.ObjectChangeViewSet) router.register('job-results', views.JobResultViewSet) router.register('content-types', views.ContentTypeViewSet) router.register('notifications', views.NotificationViewSet, basename='notifications') +router.register('review-requests', views.ReviewRequestViewSet, basename='review-requests') app_name = 'extras-api' urlpatterns = router.urls diff --git a/netbox/extras/api/views.py b/netbox/extras/api/views.py index b53d920ea..90b6b64e8 100644 --- a/netbox/extras/api/views.py +++ b/netbox/extras/api/views.py @@ -15,7 +15,7 @@ from extras import filtersets from extras.choices import JobResultStatusChoices from extras.models import * from extras.models import CustomField -from extras.models.staging import Notification +from extras.models.staging import Notification, ReviewRequest from extras.reports import get_report, get_reports, run_report from extras.scripts import get_script, get_scripts, run_script from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired @@ -406,6 +406,7 @@ class NotificationViewSet(ReadOnlyModelViewSet): serializer_class = serializers.NotificationSerializer def get_queryset(self): + # TODO: Maybe we want to drop the filter if an admin is querying. return Notification.objects.filter(user__id=self.request.user.id).order_by('created') def partial_update(self, request, pk=None): @@ -419,3 +420,15 @@ class NotificationViewSet(ReadOnlyModelViewSet): n = get_object_or_404(self.get_queryset(), pk=pk) n.delete() return Response(status=status.HTTP_200_OK) + + +class ReviewRequestViewSet(ReadOnlyModelViewSet): + + permission_classes = [IsAuthenticatedOrLoginNotRequired] + serializer_class = serializers.ReviewRequestSerializer + + def get_queryset(self): + return ReviewRequest.objects.filter(owner__id=self.request.user.id).order_by('last_updated') + + def create(self, create): + pass diff --git a/netbox/extras/choices.py b/netbox/extras/choices.py index 92d09e2ad..092736f06 100644 --- a/netbox/extras/choices.py +++ b/netbox/extras/choices.py @@ -199,3 +199,27 @@ class ChangeActionChoices(ChoiceSet): (ACTION_UPDATE, 'Update'), (ACTION_DELETE, 'Delete'), ) + + +class ReviewRequestStateChoices(ChoiceSet): + + STATE_UNDER_REVIEW = 'under_review' + STATE_APPROVED = 'approved' + STATE_DENIED = 'denied' + + CHOICES = ( + (STATE_UNDER_REVIEW, 'Under Review'), + (STATE_APPROVED, 'Approved'), + (STATE_DENIED, 'Denied') + ) + + +class ReviewRequestStatusChoices(ChoiceSet): + + STATUS_OPEN = 'open' + STATUS_CLOSED = 'closed' + + CHOICES = ( + (STATUS_OPEN, 'Open'), + (STATUS_CLOSED, 'Closed') + ) diff --git a/netbox/extras/models/staging.py b/netbox/extras/models/staging.py index 63f194a2c..943f8ce67 100644 --- a/netbox/extras/models/staging.py +++ b/netbox/extras/models/staging.py @@ -7,7 +7,9 @@ from django.db import models, transaction from django.conf import settings from django.urls import reverse -from extras.choices import ChangeActionChoices +from extras.choices import ChangeActionChoices, \ + ReviewRequestStateChoices, \ + ReviewRequestStatusChoices from netbox.models import ChangeLoggedModel, NetBoxModel from utilities.utils import deserialize_object @@ -147,3 +149,45 @@ class Notification(NetBoxModel): class Meta: ordering = ('pk',) + + +class ReviewRequest(ChangeLoggedModel): + + owner = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + related_name='review_requests' + ) + + reviewer = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + related_name='assigned_review_requests' + ) + + branch = models.ForeignKey( + to=Branch, + on_delete=models.CASCADE, + related_name='review_request' + ) + + status = models.CharField( + max_length=256, + choices=ReviewRequestStatusChoices, + default=ReviewRequestStatusChoices.STATUS_OPEN + ) + + state = models.CharField( + max_length=256, + choices=ReviewRequestStateChoices, + default=ReviewRequestStateChoices.STATE_UNDER_REVIEW + ) + + def __str__(self): + return f'[OwnerId: {self.owner.pk}, ReviewerId: {self.reviewer.id} {self.status}/{self.state}] {self.branch}' + + def get_absolute_url(self): + return reverse('extras-api:review-requests-detail', args=[self.pk]) + + class Meta: + ordering = ('pk',)