Skip to content

Commit 27e70d9

Browse files
committed
[fix] Rebase
1 parent 80288b6 commit 27e70d9

File tree

13 files changed

+223
-162
lines changed

13 files changed

+223
-162
lines changed

openwisp_controller/config/api/filters.py

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
from django_filters import rest_framework as filters
55
from django_filters.rest_framework import DjangoFilterBackend
66
from rest_framework.exceptions import ValidationError
7-
from reversion.models import Version
87
from swapper import load_model
98

109
from openwisp_users.api.filters import OrganizationManagedFilter
@@ -143,18 +142,3 @@ def __init__(self, *args, **kwargs):
143142

144143
class Meta(BaseConfigAPIFilter.Meta):
145144
model = DeviceGroup
146-
147-
148-
class ReversionFilter(BaseConfigAPIFilter):
149-
model = filters.CharFilter(field_name="content_type__model", lookup_expr="iexact")
150-
151-
def _set_valid_filterform_labels(self):
152-
self.filters["model"].label = _("Model")
153-
154-
def __init__(self, *args, **kwargs):
155-
super().__init__(*args, **kwargs)
156-
self._set_valid_filterform_labels()
157-
158-
class Meta:
159-
model = Version
160-
fields = ["model"]

openwisp_controller/config/api/serializers.py

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -379,13 +379,13 @@ def update(self, instance, validated_data):
379379
return instance
380380

381381

382-
class ReversionSerializer(BaseSerializer):
383-
user_id = serializers.SerializerMethodField()
382+
class VersionSerializer(BaseSerializer):
383+
user_id = serializers.CharField(source="revision.user_id", read_only=True)
384384
date_created = serializers.DateTimeField(
385385
source="revision.date_created", read_only=True
386386
)
387387
comment = serializers.CharField(source="revision.comment", read_only=True)
388-
content_type = serializers.SerializerMethodField()
388+
content_type = serializers.CharField(source="revision.content_type", read_only=True)
389389

390390
class Meta:
391391
model = Version
@@ -402,9 +402,3 @@ class Meta:
402402
"date_created",
403403
"comment",
404404
]
405-
406-
def get_user_id(self, obj):
407-
return getattr(obj.revision, "user_id", None)
408-
409-
def get_content_type(self, obj):
410-
return getattr(obj.content_type, "model", None)

openwisp_controller/config/api/urls.py

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,21 @@ def get_api_urls(api_views):
1313
"""
1414
if getattr(settings, "OPENWISP_CONTROLLER_API", True):
1515
return [
16+
path(
17+
"controller/<str:model>/revision/",
18+
api_views.revision_list,
19+
name="revision_list",
20+
),
21+
path(
22+
"controller/<str:model>/revision/<str:pk>/",
23+
api_views.version_detail,
24+
name="version_detail",
25+
),
26+
path(
27+
"controller/<str:model>/revision/<str:pk>/restore/",
28+
api_views.revision_restore,
29+
name="revision_restore",
30+
),
1631
path(
1732
"controller/template/",
1833
api_views.template_list,
@@ -83,21 +98,6 @@ def get_api_urls(api_views):
8398
api_download_views.download_device_config,
8499
name="download_device_config",
85100
),
86-
path(
87-
'controller/reversion/',
88-
api_views.reversion_list,
89-
name='reversion_list',
90-
),
91-
path(
92-
'controller/reversion/<str:pk>/',
93-
api_views.reversion_detail,
94-
name='reversion_detail',
95-
),
96-
path(
97-
'controller/reversion/<str:pk>/restore/',
98-
api_views.reversion_restore,
99-
name='reversion_restore',
100-
),
101101
]
102102
else:
103103
return []

openwisp_controller/config/api/views.py

Lines changed: 67 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import reversion
22
from cache_memoize import cache_memoize
33
from django.core.exceptions import ObjectDoesNotExist
4+
from django.db import transaction
45
from django.db.models import F, Q
56
from django.http import Http404
7+
from django.shortcuts import get_list_or_404
68
from django.urls.base import reverse
79
from django_filters.rest_framework import DjangoFilterBackend
810
from rest_framework import pagination, serializers, status
@@ -19,21 +21,20 @@
1921

2022
from openwisp_users.api.permissions import DjangoModelPermissions
2123

22-
from ...mixins import ProtectedAPIMixin
24+
from ...mixins import AutoRevisionMixin, ProtectedAPIMixin
2325
from .filters import (
2426
DeviceGroupListFilter,
2527
DeviceListFilter,
2628
DeviceListFilterBackend,
27-
ReversionFilter,
2829
TemplateListFilter,
2930
VPNListFilter,
3031
)
3132
from .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):
341368
devicegroup_list = DeviceGroupListCreateView.as_view()
342369
devicegroup_detail = DeviceGroupDetailView.as_view()
343370
devicegroup_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()

openwisp_controller/config/tests/test_api.py

Lines changed: 29 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1636,41 +1636,48 @@ def test_device_patch_with_templates_of_same_org(self):
16361636
self.assertEqual(d1.config.templates.count(), 2)
16371637
self.assertEqual(r.data["config"]["templates"], [t1.id, t2.id])
16381638

1639-
def test_reversion_list_and_restore_api(self):
1639+
def test_revision_list_and_restore_api(self):
16401640
org = self._get_org()
1641+
model_slug = "device"
16411642
with reversion.create_revision():
16421643
device = self._create_device(
1643-
organization=org, name="test", _is_deactivated=True
1644+
organization=org,
1645+
name="test",
16441646
)
16451647
path = reverse("config_api:device_detail", args=[device.pk])
1646-
response = self.client.delete(path)
1647-
self.assertEqual(response.status_code, 204)
1648-
self.assertEqual(Device.objects.count(), 0)
1649-
1650-
path = reverse("config_api:reversion_list")
1651-
response = self.client.get(path)
1652-
response_json = response.json()
1653-
version_id = response_json[0]["id"]
1648+
data = dict(name="change-test-device")
1649+
response = self.client.patch(path, data, content_type="application/json")
16541650
self.assertEqual(response.status_code, 200)
1655-
self.assertEqual(len(response_json), 1)
1651+
self.assertEqual(response.data["name"], "change-test-device")
16561652

1657-
with self.subTest("Test filter reversion list with model name"):
1658-
params = {"id": 1, "model": "Device"}
1659-
response = self.client.get(path, params)
1653+
with self.subTest("Test revision list"):
1654+
path = reverse("config_api:revision_list", args=[model_slug])
1655+
response = self.client.get(path)
1656+
response_json = response.json()
1657+
version_id = response_json[1]["id"]
1658+
revision_id = response_json[1]["revision_id"]
1659+
self.assertEqual(response.status_code, 200)
1660+
self.assertEqual(len(response_json), 2)
1661+
1662+
with self.subTest("Test revision list filter by revision id"):
1663+
path = reverse("config_api:revision_list", args=[model_slug])
1664+
response = self.client.get(f"{path}?revision_id={revision_id}")
1665+
response_json = response.json()
16601666
self.assertEqual(response.status_code, 200)
1661-
self.assertEqual(len(response.json()), 1)
1662-
self.assertEqual(response.json()[0]["object_id"], str(device.pk))
1667+
self.assertEqual(len(response_json), 2)
16631668

1664-
with self.subTest("Test reversion detail"):
1665-
path = reverse("config_api:reversion_detail", args=[version_id])
1669+
with self.subTest("Test version detail"):
1670+
path = reverse("config_api:version_detail", args=[model_slug, version_id])
16661671
response = self.client.get(path)
16671672
self.assertEqual(response.status_code, 200)
16681673
self.assertEqual(response.json()["id"], version_id)
16691674
self.assertEqual(response.json()["object_id"], str(device.pk))
16701675

1671-
with self.subTest("Test reversion restore view"):
1672-
path = reverse("config_api:reversion_restore", args=[version_id])
1676+
with self.subTest("Test revision restore view"):
1677+
revision_id = response_json[1]["revision_id"]
1678+
path = reverse(
1679+
"config_api:revision_restore", args=[model_slug, revision_id]
1680+
)
16731681
response = self.client.post(path)
16741682
self.assertEqual(response.status_code, 200)
1675-
self.assertEqual(Device.objects.count(), 1)
1676-
self.assertEqual(Device.objects.first().id, device.pk)
1683+
self.assertEqual(Device.objects.get(name="test").pk, device.pk)

0 commit comments

Comments
 (0)