Explorar o código

Added API serializer for ObjectChange

Jeremy Stretch %!s(int64=7) %!d(string=hai) anos
pai
achega
23f91274d6

+ 30 - 4
netbox/extras/api/serializers.py

@@ -6,11 +6,12 @@ from taggit.models import Tag
 
 from dcim.api.serializers import NestedDeviceSerializer, NestedRackSerializer, NestedSiteSerializer
 from dcim.models import Device, Rack, Site
-from extras.constants import ACTION_CHOICES, GRAPH_TYPE_CHOICES
-from extras.models import ExportTemplate, Graph, ImageAttachment, ReportResult, TopologyMap, UserAction
-from users.api.serializers import NestedUserSerializer
-from utilities.api import ChoiceFieldSerializer, ContentTypeFieldSerializer, ValidatedModelSerializer
+from extras.models import ExportTemplate, Graph, ImageAttachment, ObjectChange, ReportResult, TopologyMap, UserAction
 from extras.constants import *
+from users.api.serializers import NestedUserSerializer
+from utilities.api import (
+    ChoiceFieldSerializer, ContentTypeFieldSerializer, get_serializer_for_model, ValidatedModelSerializer,
+)
 
 
 #
@@ -155,6 +156,31 @@ class ReportDetailSerializer(ReportSerializer):
     result = ReportResultSerializer()
 
 
+#
+# Change logging
+#
+
+class ObjectChangeSerializer(serializers.ModelSerializer):
+    user = NestedUserSerializer(read_only=True)
+    content_type = ContentTypeFieldSerializer(read_only=True)
+    changed_object = serializers.SerializerMethodField(read_only=True)
+
+    class Meta:
+        model = ObjectChange
+        fields = ['id', 'time', 'user', 'user_name', 'action', 'content_type', 'changed_object', 'object_data']
+
+    def get_changed_object(self, obj):
+        """
+        Serialize a nested representation of the changed object.
+        """
+        serializer = get_serializer_for_model(obj.changed_object, prefix='Nested')
+        if serializer is None:
+            return obj.object_repr
+        context = {'request': self.context['request']}
+        data = serializer(obj.changed_object, context=context).data
+        return data
+
+
 #
 # User actions
 #

+ 3 - 0
netbox/extras/api/urls.py

@@ -37,6 +37,9 @@ router.register(r'image-attachments', views.ImageAttachmentViewSet)
 # Reports
 router.register(r'reports', views.ReportViewSet, base_name='report')
 
+# Change logging
+router.register(r'object-changes', views.ObjectChangeViewSet)
+
 # Recent activity
 router.register(r'recent-activity', views.RecentActivityViewSet)
 

+ 16 - 1
netbox/extras/api/views.py

@@ -11,7 +11,9 @@ from rest_framework.viewsets import ReadOnlyModelViewSet, ViewSet
 from taggit.models import Tag
 
 from extras import filters
-from extras.models import CustomField, ExportTemplate, Graph, ImageAttachment, ReportResult, TopologyMap, UserAction
+from extras.models import (
+    CustomField, ExportTemplate, Graph, ImageAttachment, ObjectChange, ReportResult, TopologyMap, UserAction,
+)
 from extras.reports import get_report, get_reports
 from utilities.api import FieldChoicesViewSet, IsAuthenticatedOrLoginNotRequired, ModelViewSet
 from . import serializers
@@ -206,6 +208,19 @@ class ReportViewSet(ViewSet):
         return Response(serializer.data)
 
 
+#
+# Change logging
+#
+
+class ObjectChangeViewSet(ReadOnlyModelViewSet):
+    """
+    Retrieve a list of recent changes.
+    """
+    queryset = ObjectChange.objects.select_related('user')
+    serializer_class = serializers.ObjectChangeSerializer
+    filter_class = filters.ObjectChangeFilter
+
+
 #
 # User activity
 #

+ 20 - 1
netbox/extras/filters.py

@@ -8,7 +8,7 @@ from taggit.models import Tag
 
 from dcim.models import Site
 from .constants import CF_FILTER_DISABLED, CF_FILTER_EXACT, CF_TYPE_BOOLEAN, CF_TYPE_SELECT
-from .models import CustomField, Graph, ExportTemplate, TopologyMap, UserAction
+from .models import CustomField, Graph, ExportTemplate, ObjectChange, TopologyMap, UserAction
 
 
 class CustomFieldFilter(django_filters.Filter):
@@ -124,6 +124,25 @@ class TopologyMapFilter(django_filters.FilterSet):
         fields = ['name', 'slug']
 
 
+class ObjectChangeFilter(django_filters.FilterSet):
+    q = django_filters.CharFilter(
+        method='search',
+        label='Search',
+    )
+
+    class Meta:
+        model = ObjectChange
+        fields = ['user_name', 'action', 'content_type', 'object_repr']
+
+    def search(self, queryset, name, value):
+        if not value.strip():
+            return queryset
+        return queryset.filter(
+            Q(user_name__icontains=value) |
+            Q(object_repr__icontains=value)
+        )
+
+
 class UserActionFilter(django_filters.FilterSet):
     username = django_filters.ModelMultipleChoiceFilter(
         name='user__username',

+ 16 - 0
netbox/utilities/api.py

@@ -16,6 +16,8 @@ from rest_framework.response import Response
 from rest_framework.serializers import Field, ModelSerializer, RelatedField, ValidationError
 from rest_framework.viewsets import GenericViewSet, ViewSet
 
+from .utils import dynamic_import
+
 WRITE_OPERATIONS = ['create', 'update', 'partial_update', 'delete']
 
 
@@ -24,6 +26,20 @@ class ServiceUnavailable(APIException):
     default_detail = "Service temporarily unavailable, please try again later."
 
 
+def get_serializer_for_model(model, prefix=''):
+    """
+    Dynamically resolve and return the appropriate serializer for a model.
+    """
+    app_name, model_name = model._meta.label.split('.')
+    serializer_name = '{}.api.serializers.{}{}Serializer'.format(
+        app_name, prefix, model_name
+    )
+    try:
+        return dynamic_import(serializer_name)
+    except ImportError:
+        return None
+
+
 #
 # Authentication
 #