Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix error raised when create_ops=[] or update_ops=[] #313

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.6, 3.7, 3.8, 3.9]
python-version: [3.7, 3.8, 3.9, "3.10", 3.11, 3.12]

steps:
- uses: actions/checkout@v1
Expand Down
27 changes: 19 additions & 8 deletions django_restql/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,9 @@ def BaseNestedFieldSerializerFactory(

def join_words(words, many='are', single='is'):
word_list = ["`" + word + "`" for word in words]
sentence = " & ".join([", ".join(word_list[:-1]), word_list[-1]])

if len(words) > 1:
sentence = " & ".join([", ".join(word_list[:-1]), word_list[-1]])
return "%s %s" % (many, sentence)
elif len(words) == 1:
return "%s %s" % (single, word_list[0])
Expand Down Expand Up @@ -130,7 +131,7 @@ def run_pk_list_validation(self, pks):
many=True
).run_validation(pks)

def run_data_list_validation(self, data, partial=None):
def run_data_list_validation(self, data, partial=None, operation=None):
ListField().run_validation(data)
model = self.parent.Meta.model
rel = getattr(model, self.source).rel
Expand All @@ -142,7 +143,7 @@ def run_data_list_validation(self, data, partial=None):
data=data,
many=True,
partial=partial,
context=self.context
context={**self.context, "parent_operation": operation}
)

# Remove parent field(field_name) for validation purpose
Expand All @@ -157,7 +158,7 @@ def run_data_list_validation(self, data, partial=None):
data=data,
many=True,
partial=partial,
context=self.context
context={**self.context, "parent_operation": operation}
)

# Check if a serializer is valid
Expand All @@ -169,7 +170,8 @@ def run_add_list_validation(self, data):
def run_create_list_validation(self, data):
self.run_data_list_validation(
data,
partial=self.is_partial(False)
partial=self.is_partial(False),
operation=CREATE
)

def run_remove_list_validation(self, data):
Expand All @@ -190,7 +192,8 @@ def run_update_list_validation(self, data):
values = list(data.values())
self.run_data_list_validation(
values,
partial=self.is_partial(True)
partial=self.is_partial(True),
operation=UPDATE
)

def run_data_validation(self, data, allowed_ops):
Expand Down Expand Up @@ -226,8 +229,14 @@ def run_data_validation(self, data, allowed_ops):

def to_internal_value(self, data):
if self.child.root.instance is None:
self.run_data_validation(data, create_ops)
parent_operation = self.context.get("parent_operation")
if parent_operation == "update":
# Definitely an update
self.run_data_validation(data, update_ops)
else:
self.run_data_validation(data, create_ops)
else:
# Definitely an update
self.run_data_validation(data, update_ops)
return data

Expand Down Expand Up @@ -265,13 +274,15 @@ def run_pk_validation(self, pk):
return validator.run_validation(pk)

def run_data_validation(self, data):
parent_operation = self.context.get("parent_operation")

child_serializer = serializer_class(
**self.validation_kwargs,
data=data,
partial=self.is_partial(
# Use the partial value passed, if it's not passed
# Use the one from the top level parent
self.root.partial
True if parent_operation == UPDATE else False
),
context=self.context
)
Expand Down
27 changes: 18 additions & 9 deletions django_restql/mixins.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from django.db.models import Prefetch
from django.core.exceptions import ObjectDoesNotExist
from django.db.models.fields.related import ManyToManyRel, ManyToOneRel
from django.http import QueryDict
from django.utils.functional import cached_property
Expand Down Expand Up @@ -597,7 +598,7 @@ def create_writable_foreignkey_related(self, data):
# Reject partial update by default(if partial kwarg is not passed)
# since we need all required fields when creating object
partial=nested_field_serializer.is_partial(False),
context=self.context
context={**self.context, "parent_operation": CREATE}
)
serializer.is_valid(raise_exception=True)
if value is None:
Expand All @@ -622,7 +623,7 @@ def bulk_create_objs(self, field, data):
# Reject partial update by default(if partial kwarg is not passed)
# since we need all required fields when creating object
partial=nested_field_serializer.is_partial(False),
context=self.context,
context={**self.context, "parent_operation": CREATE},
)
serializer.is_valid(raise_exception=True)
obj = serializer.save()
Expand Down Expand Up @@ -770,7 +771,7 @@ def update_writable_foreignkey_related(self, instance, data):
# Allow partial update by default(if partial kwarg is not passed)
# since this is nested update
partial=nested_field_serializer.is_partial(True),
context=self.context
context={**self.context, "parent_operation": UPDATE}
)
serializer.is_valid(raise_exception=True)
if values is None:
Expand Down Expand Up @@ -798,7 +799,7 @@ def bulk_create_many_to_many_related(self, field, nested_obj, data):
# Reject partial update by default(if partial kwarg is not passed)
# since we need all required fields when creating object
partial=nested_field_serializer.is_partial(False),
context=self.context
context={**self.context, "parent_operation": CREATE}
)
serializer.is_valid(raise_exception=True)
obj = serializer.save()
Expand All @@ -819,7 +820,7 @@ def bulk_create_many_to_one_related(self, field, nested_obj, data):
# Reject partial update by default(if partial kwarg is not passed)
# since we need all required fields when creating object
partial=nested_field_serializer.is_partial(False),
context=self.context
context={**self.context, "parent_operation": CREATE}
)
serializer.is_valid(raise_exception=True)
obj = serializer.save()
Expand All @@ -834,15 +835,19 @@ def bulk_update_many_to_many_related(self, field, nested_obj, data):
serializer_class = nested_field_serializer.serializer_class
kwargs = nested_field_serializer.validation_kwargs
for pk, values in data.items():
obj = nested_obj.get(pk=pk)
try:
obj = nested_obj.get(pk=pk)
except ObjectDoesNotExist:
# This pk does't belong to nested field
continue
serializer = serializer_class(
obj,
**kwargs,
data=values,
# Allow partial update by default(if partial kwarg is not passed)
# since this is nested update
partial=nested_field_serializer.is_partial(True),
context=self.context
context={**self.context, "parent_operation": UPDATE}
)
serializer.is_valid(raise_exception=True)
obj = serializer.save()
Expand All @@ -858,7 +863,11 @@ def bulk_update_many_to_one_related(self, field, instance, data):
foreignkey = getattr(model, field).field.name
nested_obj = getattr(instance, field)
for pk, values in data.items():
obj = nested_obj.get(pk=pk)
try:
obj = nested_obj.get(pk=pk)
except ObjectDoesNotExist:
# This pk does't belong to nested field
continue
values.update({foreignkey: instance.pk})
serializer = serializer_class(
obj,
Expand All @@ -867,7 +876,7 @@ def bulk_update_many_to_one_related(self, field, instance, data):
# Allow partial update by default(if partial kwarg is not passed)
# since this is nested update
partial=nested_field_serializer.is_partial(True),
context=self.context
context={**self.context, "parent_operation": UPDATE}
)
serializer.is_valid(raise_exception=True)
obj = serializer.save()
Expand Down
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ def get_info(info_name):
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10',
'Programming Language :: Python :: 3.11',
'Programming Language :: Python :: 3.12',
],
test_suite='runtests',
)
39 changes: 36 additions & 3 deletions tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -2535,7 +2535,7 @@ def test_patch_with_update_operation_missing_one_required_nested_field(self):
}
)

def test_patch_on_deep_nested_fields(self):
def test_patch_with_mixed_operations(self):
url = reverse_lazy("wstudent-detail", args=[self.student.id])
data = {
"name": "yezy",
Expand All @@ -2544,7 +2544,34 @@ def test_patch_on_deep_nested_fields(self):
"name": "Programming",
"code": "CS50",
"books": {
"remove": [1]
"update": {
1: {
"title": "React Programming",
"author": "K.Kennedy",
"genres": {
# update_operations applies here since the parent is "update"
"remove": [],
"add": [],
"create": [
{"title": "Modern Programming", "description": "New tools"}
],
"update": {}
}
}
},
"create": [
{
"title": "CS Foundation",
"author": "T.Howard",
"genres": {
# create_operations applies here since the parent is "create"
"add": [],
"create": [
{"title": "Classical Programming", "description": "Classical tech tools"}
]
}
}
]
}
}
}
Expand All @@ -2557,7 +2584,13 @@ def test_patch_on_deep_nested_fields(self):
'course': {
'name': 'Programming', 'code': 'CS50',
'books': [
{'title': 'Basic Data Structures', 'author': 'S.Mobit', "genres": []}
{'title': 'React Programming', 'author': 'K.Kennedy', "genres": [
{"title": "Modern Programming", "description": "New tools"}
]},
{'title': 'Basic Data Structures', 'author': 'S.Mobit', 'genres': []},
{'title': 'CS Foundation', 'author': 'T.Howard', "genres": [
{"title": "Classical Programming", "description": "Classical tech tools"}
]}
],
'instructor': None
},
Expand Down
2 changes: 1 addition & 1 deletion tests/testapp/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ class Meta:

############### Serializers for Nested Data Mutation Testing ##############
class WritableBookSerializer(DynamicFieldsMixin, NestedModelSerializer):
genres = NestedField(GenreSerializer, many=True, required=False)
genres = NestedField(GenreSerializer, many=True, required=False, partial=False)

class Meta:
model = Book
Expand Down
8 changes: 4 additions & 4 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,21 @@

[tox]
envlist =
py{36,37}-dj{111}-drf{35,36,37,38,39,310,311}
py{36,37}-dj{20,21,22}-drf{37,38,39,310,311}
py{37}-dj{111}-drf{35,36,37,38,39,310,311}
py{37}-dj{20,21,22}-drf{37,38,39,310,311}
py{38,39}-dj{22}-drf{37,38,39,310,311,312}
py{36,37,38,39}-dj{30}-drf{310,311,312}
py{36,37,38,39,310}-dj{31,32}-drf{311,312,313,314}
py{38,39,310,311}-dj{40,41}-drf{313,314}
py{38,39,310,311,312}-dj{40,41}-drf{313,314}

[gh-actions]
python =
3.6: py36
3.7: py37
3.8: py38
3.9: py39
3.10: py310
3.11: py311
3.12: py312

DJANGO =
1.11: dj111
Expand Down
Loading