11import reversion
22from cache_memoize import cache_memoize
33from django .core .exceptions import ObjectDoesNotExist
4+ from django .db import transaction
45from django .db .models import F , Q
56from django .http import Http404
7+ from django .shortcuts import get_list_or_404
68from django .urls .base import reverse
79from django_filters .rest_framework import DjangoFilterBackend
810from rest_framework import pagination , serializers , status
1921
2022from openwisp_users .api .permissions import DjangoModelPermissions
2123
22- from ...mixins import ProtectedAPIMixin
24+ from ...mixins import AutoRevisionMixin , ProtectedAPIMixin
2325from .filters import (
2426 DeviceGroupListFilter ,
2527 DeviceListFilter ,
2628 DeviceListFilterBackend ,
27- ReversionFilter ,
2829 TemplateListFilter ,
2930 VPNListFilter ,
3031)
3132from .serializers import (
3233 DeviceDetailSerializer ,
3334 DeviceGroupSerializer ,
3435 DeviceListSerializer ,
35- ReversionSerializer ,
3636 TemplateSerializer ,
37+ VersionSerializer ,
3738 VpnSerializer ,
3839)
3940
@@ -53,28 +54,30 @@ class ListViewPagination(pagination.PageNumberPagination):
5354 max_page_size = 100
5455
5556
56- class TemplateListCreateView (ProtectedAPIMixin , ListCreateAPIView ):
57+ class TemplateListCreateView (ProtectedAPIMixin , AutoRevisionMixin , ListCreateAPIView ):
5758 serializer_class = TemplateSerializer
5859 queryset = Template .objects .prefetch_related ("tags" ).order_by ("-created" )
5960 pagination_class = ListViewPagination
6061 filter_backends = [DjangoFilterBackend ]
6162 filterset_class = TemplateListFilter
6263
6364
64- class TemplateDetailView (ProtectedAPIMixin , RetrieveUpdateDestroyAPIView ):
65+ class TemplateDetailView (
66+ ProtectedAPIMixin , AutoRevisionMixin , RetrieveUpdateDestroyAPIView
67+ ):
6568 serializer_class = TemplateSerializer
6669 queryset = Template .objects .all ()
6770
6871
69- class VpnListCreateView (ProtectedAPIMixin , ListCreateAPIView ):
72+ class VpnListCreateView (ProtectedAPIMixin , AutoRevisionMixin , ListCreateAPIView ):
7073 serializer_class = VpnSerializer
7174 queryset = Vpn .objects .select_related ("subnet" ).order_by ("-created" )
7275 pagination_class = ListViewPagination
7376 filter_backends = [DjangoFilterBackend ]
7477 filterset_class = VPNListFilter
7578
7679
77- class VpnDetailView (ProtectedAPIMixin , RetrieveUpdateDestroyAPIView ):
80+ class VpnDetailView (ProtectedAPIMixin , AutoRevisionMixin , RetrieveUpdateDestroyAPIView ):
7881 serializer_class = VpnSerializer
7982 queryset = Vpn .objects .all ()
8083
@@ -87,7 +90,7 @@ def has_object_permission(self, request, view, obj):
8790 return perm and not obj .is_deactivated ()
8891
8992
90- class DeviceListCreateView (ProtectedAPIMixin , ListCreateAPIView ):
93+ class DeviceListCreateView (ProtectedAPIMixin , AutoRevisionMixin , ListCreateAPIView ):
9194 """
9295 Templates: Templates flagged as required will be added automatically
9396 to the `config` of a device and cannot be unassigned.
@@ -102,7 +105,9 @@ class DeviceListCreateView(ProtectedAPIMixin, ListCreateAPIView):
102105 filterset_class = DeviceListFilter
103106
104107
105- class DeviceDetailView (ProtectedAPIMixin , RetrieveUpdateDestroyAPIView ):
108+ class DeviceDetailView (
109+ ProtectedAPIMixin , AutoRevisionMixin , RetrieveUpdateDestroyAPIView
110+ ):
106111 """
107112 Templates: Templates flagged as _required_ will be added automatically
108113 to the `config` of a device and cannot be unassigned.
@@ -129,7 +134,7 @@ def get_serializer_context(self):
129134 return context
130135
131136
132- class DeviceActivateView (ProtectedAPIMixin , GenericAPIView ):
137+ class DeviceActivateView (ProtectedAPIMixin , AutoRevisionMixin , GenericAPIView ):
133138 serializer_class = serializers .Serializer
134139 queryset = Device .objects .filter (_is_deactivated = True )
135140
@@ -142,7 +147,7 @@ def post(self, request, *args, **kwargs):
142147 return Response (serializer .data , status = status .HTTP_200_OK )
143148
144149
145- class DeviceDeactivateView (ProtectedAPIMixin , GenericAPIView ):
150+ class DeviceDeactivateView (ProtectedAPIMixin , AutoRevisionMixin , GenericAPIView ):
146151 serializer_class = serializers .Serializer
147152 queryset = Device .objects .filter (_is_deactivated = False )
148153
@@ -155,15 +160,19 @@ def post(self, request, *args, **kwargs):
155160 return Response (serializer .data , status = status .HTTP_200_OK )
156161
157162
158- class DeviceGroupListCreateView (ProtectedAPIMixin , ListCreateAPIView ):
163+ class DeviceGroupListCreateView (
164+ ProtectedAPIMixin , AutoRevisionMixin , ListCreateAPIView
165+ ):
159166 serializer_class = DeviceGroupSerializer
160167 queryset = DeviceGroup .objects .prefetch_related ("templates" ).order_by ("-created" )
161168 pagination_class = ListViewPagination
162169 filter_backends = [DjangoFilterBackend ]
163170 filterset_class = DeviceGroupListFilter
164171
165172
166- class DeviceGroupDetailView (ProtectedAPIMixin , RetrieveUpdateDestroyAPIView ):
173+ class DeviceGroupDetailView (
174+ ProtectedAPIMixin , AutoRevisionMixin , RetrieveUpdateDestroyAPIView
175+ ):
167176 serializer_class = DeviceGroupSerializer
168177 queryset = DeviceGroup .objects .select_related ("organization" ).order_by ("-created" )
169178
@@ -177,7 +186,7 @@ def get_cached_devicegroup_args_rewrite(cls, org_slugs, common_name):
177186 return url
178187
179188
180- class DeviceGroupCommonName (ProtectedAPIMixin , RetrieveAPIView ):
189+ class DeviceGroupCommonName (ProtectedAPIMixin , AutoRevisionMixin , RetrieveAPIView ):
181190 serializer_class = DeviceGroupSerializer
182191 queryset = DeviceGroup .objects .select_related ("organization" ).order_by ("-created" )
183192 # Not setting lookup_field makes DRF raise error. but it is not used
@@ -294,39 +303,57 @@ def certificate_delete_invalidates_cache(cls, organization_id, common_name):
294303 cls .get_device_group .invalidate (cls , org_slug , common_name )
295304
296305
297- class ReversionListView (ProtectedAPIMixin , ListAPIView ):
298- serializer_class = ReversionSerializer
299- queryset = Version .objects .select_related (' revision' ).order_by (
300- ' -revision__date_created'
306+ class RevisionListView (ProtectedAPIMixin , ListAPIView ):
307+ serializer_class = VersionSerializer
308+ queryset = Version .objects .select_related (" revision" ).order_by (
309+ " -revision__date_created"
301310 )
302- filter_backends = [DjangoFilterBackend ]
303- filterset_class = ReversionFilter
311+
312+ def get_queryset (self ):
313+ model = self .kwargs .get ("model" ).lower ()
314+ queryset = self .queryset .filter (content_type__model = model )
315+ revision_id = self .request .query_params .get ("revision_id" )
316+ if revision_id :
317+ queryset = queryset .filter (revision_id = revision_id )
318+ return self .queryset .filter (content_type__model = model )
304319
305320
306- class ReversionDetailView (ProtectedAPIMixin , RetrieveAPIView ):
307- serializer_class = ReversionSerializer
308- queryset = Version .objects .select_related (' revision' ).order_by (
309- ' -revision__date_created'
321+ class VersionDetailView (ProtectedAPIMixin , RetrieveAPIView ):
322+ serializer_class = VersionSerializer
323+ queryset = Version .objects .select_related (" revision" ).order_by (
324+ " -revision__date_created"
310325 )
311- lookup_field = 'pk'
312326
327+ def get_queryset (self ):
328+ model = self .kwargs .get ("model" ).lower ()
329+ return self .queryset .filter (content_type__model = model )
313330
314- class ReversionRestoreView (ProtectedAPIMixin , GenericAPIView ):
331+
332+ class RevisionRestoreView (ProtectedAPIMixin , GenericAPIView ):
315333 serializer_class = serializers .Serializer
316- queryset = Version .objects .select_related (' revision' ).order_by (
317- ' -revision__date_created'
334+ queryset = Version .objects .select_related (" revision" ).order_by (
335+ " -revision__date_created"
318336 )
319337
320- def post (self , request , * args , ** kwargs ):
321- version = self .get_object ()
322- with reversion .create_revision ():
323- version .revert ()
324- reversion .set_user (request .user )
325- reversion .set_comment (
326- f"Restored to previous revision: { version .revision_id } "
327- )
338+ def get_queryset (self ):
339+ model = self .kwargs .get ("model" ).lower ()
340+ return self .queryset .filter (content_type__model = model )
328341
329- serializer = ReversionSerializer (version , context = self .get_serializer_context ())
342+ def post (self , request , * args , ** kwargs ):
343+ qs = self .get_queryset ()
344+ versions = get_list_or_404 (qs , revision_id = kwargs ["pk" ])
345+ with transaction .atomic ():
346+ with reversion .create_revision ():
347+ for version in versions :
348+ version .revert ()
349+ reversion .set_user (request .user )
350+ reversion .set_comment (
351+ f"Restored to previous revision: { self .kwargs .get ('pk' )} "
352+ )
353+
354+ serializer = VersionSerializer (
355+ versions , many = True , context = self .get_serializer_context ()
356+ )
330357 return Response (serializer .data , status = status .HTTP_200_OK )
331358
332359
@@ -341,6 +368,6 @@ def post(self, request, *args, **kwargs):
341368devicegroup_list = DeviceGroupListCreateView .as_view ()
342369devicegroup_detail = DeviceGroupDetailView .as_view ()
343370devicegroup_commonname = DeviceGroupCommonName .as_view ()
344- reversion_list = ReversionListView .as_view ()
345- reversion_detail = ReversionDetailView .as_view ()
346- reversion_restore = ReversionRestoreView .as_view ()
371+ revision_list = RevisionListView .as_view ()
372+ version_detail = VersionDetailView .as_view ()
373+ revision_restore = RevisionRestoreView .as_view ()
0 commit comments