Skip to content

Commit a574e1a

Browse files
committed
[fix] Fixed ValidatedModelSerializer to handle update requests #633
Fixes #633
1 parent 1c893d6 commit a574e1a

4 files changed

Lines changed: 101 additions & 30 deletions

File tree

openwisp_utils/api/serializers.py

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from copy import copy
2+
13
from django.core.exceptions import ImproperlyConfigured
24
from django.db import models
35

@@ -19,16 +21,12 @@ def validate(self, data):
1921
Allows to avoid having to duplicate model validation logic in the
2022
REST API.
2123
"""
22-
instance = self.instance
23-
# if instance is empty (eg: creation)
24-
# simulate for validation purposes
25-
if not instance:
26-
Model = self.Meta.model
27-
instance = Model()
28-
for key, value in data.items():
29-
# avoid direct assignment for m2m (not allowed)
30-
if not isinstance(Model._meta.get_field(key), models.ManyToManyField):
31-
setattr(instance, key, value)
24+
Model = self.Meta.model
25+
instance = copy(self.instance) if self.instance else Model()
26+
for key, value in data.items():
27+
# avoid direct assignment for m2m (not allowed)
28+
if not isinstance(Model._meta.get_field(key), models.ManyToManyField):
29+
setattr(instance, key, value)
3230
# perform model validation
3331
instance.full_clean(exclude=self.exclude_validation)
3432
return data

tests/test_project/api/urls.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from django.urls import re_path
1+
from django.urls import path, re_path
22

33
from . import views
44

@@ -7,5 +7,11 @@
77
r"^receive_project/(?P<pk>[^/\?]+)/$",
88
views.receive_project,
99
name="receive_project",
10-
)
10+
),
11+
path("shelves/", views.shelf_list_create_view, name="shelf_list"),
12+
path(
13+
"shelves/<uuid:pk>/",
14+
views.shelf_retrieve_update_destroy_view,
15+
name="shelf_detail",
16+
),
1117
]

tests/test_project/api/views.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
from django.http import JsonResponse
22
from django.utils.translation import gettext_lazy as _
33
from django.views import View
4+
from rest_framework import generics
5+
from test_project.serializers import ShelfSerializer
46

5-
from ..models import Project
7+
from ..models import Project, Shelf
68

79

810
class ReceiveProjectView(View):
@@ -23,4 +25,16 @@ def get(self, request, pk):
2325
return JsonResponse({"detail": _("ok"), "name": project.name}, status=200)
2426

2527

28+
class ShelfListCreateView(generics.ListCreateAPIView):
29+
queryset = Shelf.objects.all()
30+
serializer_class = ShelfSerializer
31+
32+
33+
class ShelfRetrieveUpdateDestroyView(generics.RetrieveUpdateDestroyAPIView):
34+
queryset = Shelf.objects.all()
35+
serializer_class = ShelfSerializer
36+
37+
2638
receive_project = ReceiveProjectView.as_view()
39+
shelf_list_create_view = ShelfListCreateView.as_view()
40+
shelf_retrieve_update_destroy_view = ShelfRetrieveUpdateDestroyView.as_view()
Lines changed: 70 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
from django.conf import settings
2-
from django.core.exceptions import ValidationError
2+
from django.core.exceptions import ValidationError as DjangoValidationError
33
from django.test import TestCase
4+
from django.urls import reverse
5+
from rest_framework import status
6+
from rest_framework.exceptions import ValidationError
47
from test_project.serializers import ShelfSerializer
58

69
from ..models import Shelf
@@ -11,15 +14,9 @@ class TestApi(CreateMixin, TestCase):
1114
shelf_model = Shelf
1215
operator_permission_filter = [{"codename__endswith": "shelf"}]
1316

14-
def test_validator_pass(self):
15-
s1 = self._create_shelf(name="shelf1")
16-
serializer = ShelfSerializer(s1)
17-
result = serializer.validate(s1)
18-
self.assertIsInstance(result, Shelf)
19-
2017
def test_validator_data_dict(self):
2118
s1 = self._create_shelf(name="shelf1")
22-
data = s1.__dict__
19+
data = s1.__dict__.copy()
2320
to_delete = [
2421
"_state",
2522
"id",
@@ -30,27 +27,26 @@ def test_validator_data_dict(self):
3027
for key in to_delete:
3128
del data[key]
3229
data["writers"] = [1]
33-
serializer = ShelfSerializer()
30+
serializer = ShelfSerializer(instance=s1, data=data)
3431
data = serializer.validate(data)
3532

3633
def test_validator_fail(self):
37-
with self.assertRaises(ValidationError):
34+
with self.assertRaises(DjangoValidationError):
3835
self._create_shelf(name="Intentional_Test_Fail")
3936

4037
s1 = self._create_shelf(name="shelf1")
41-
s1.name = "Intentional_Test_Fail"
42-
serializer = ShelfSerializer(s1)
38+
serializer = ShelfSerializer(
39+
instance=s1, data={"name": "Intentional_Test_Fail"}
40+
)
4341
with self.assertRaises(ValidationError):
44-
serializer.validate(s1)
42+
serializer.is_valid(raise_exception=True)
4543

4644
def test_exclude_validation(self):
4745
s1 = self._create_shelf(name="shelf1")
48-
s1.books_type = "madeup"
49-
serializer = ShelfSerializer(s1)
46+
serializer = ShelfSerializer(instance=s1, data={"books_type": "invalid"})
5047
with self.assertRaises(ValidationError):
51-
serializer.validate(s1)
48+
serializer.is_valid(raise_exception=True)
5249
serializer.exclude_validation = ["books_type"]
53-
serializer.validate(s1)
5450

5551
def test_rest_framework_settings_override(self):
5652
drf_conf = getattr(settings, "REST_FRAMEWORK", {})
@@ -64,3 +60,60 @@ def test_rest_framework_settings_override(self):
6460
"TEST": True,
6561
},
6662
)
63+
64+
def test_crud_shelf(self):
65+
list_url = reverse("shelf_list")
66+
with self.subTest("Create"):
67+
resp = self.client.post(list_url, {"name": "shelf1"}, format="json")
68+
self.assertEqual(resp.status_code, status.HTTP_201_CREATED)
69+
self.assertEqual(Shelf.objects.count(), 1)
70+
pk = resp.data["id"]
71+
72+
shelf = Shelf.objects.get(pk=pk)
73+
detail_url = reverse("shelf_detail", args=[pk])
74+
with self.subTest("List"):
75+
resp = self.client.get(list_url)
76+
self.assertEqual(resp.status_code, status.HTTP_200_OK)
77+
self.assertIn(pk, [s["id"] for s in resp.data])
78+
79+
with self.subTest("Retrieve"):
80+
resp = self.client.get(detail_url)
81+
self.assertEqual(resp.status_code, status.HTTP_200_OK)
82+
83+
with self.subTest("Update"):
84+
resp = self.client.patch(
85+
detail_url, {"name": "shelf2"}, content_type="application/json"
86+
)
87+
self.assertEqual(resp.status_code, status.HTTP_200_OK)
88+
# ensure the DB reflects the change
89+
shelf.refresh_from_db()
90+
self.assertEqual(shelf.name, "shelf2")
91+
92+
with self.subTest("Delete"):
93+
resp = self.client.delete(detail_url)
94+
self.assertEqual(resp.status_code, status.HTTP_204_NO_CONTENT)
95+
self.assertEqual(Shelf.objects.count(), 0)
96+
97+
def test_model_validation_create_and_update(self):
98+
list_url = reverse("shelf_list")
99+
# Creating with invalid name should fail due to model.clean()
100+
resp = self.client.post(
101+
list_url, {"name": "Intentional_Test_Fail"}, format="json"
102+
)
103+
self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST)
104+
# Create valid
105+
resp = self.client.post(list_url, {"name": "valid"}, format="json")
106+
self.assertEqual(resp.status_code, status.HTTP_201_CREATED)
107+
108+
pk = resp.data["id"]
109+
detail_url = reverse("shelf_detail", args=[pk])
110+
# Updating to invalid should fail
111+
resp = self.client.patch(
112+
detail_url,
113+
{"name": "Intentional_Test_Fail"},
114+
content_type="application/json",
115+
)
116+
self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST)
117+
# Ensure DB value unchanged
118+
shelf = Shelf.objects.get(pk=pk)
119+
self.assertEqual(shelf.name, "valid")

0 commit comments

Comments
 (0)