-
-
Notifications
You must be signed in to change notification settings - Fork 7.1k
Fix #8926: ListSerializer supports instance access during validation for many=True #9879
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
base: main
Are you sure you want to change the base?
Changes from all commits
f0375ca
ac82e50
07de4b8
c402a57
66b8012
90e1a24
0acf49a
1484520
ef7e976
c665595
b61b472
22caa96
de40cb5
5176e44
c9665dd
83e9965
21417c8
781cf7e
5bdf57e
f08921e
bb6b3bb
dcb4ad1
2fa19ed
dd07e02
2a84879
7ce5b8f
e0129a3
341665c
be54fe2
71ad671
c5f6024
8156fd6
7ed28ae
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13,6 +13,7 @@ | |
| /env/ | ||
| MANIFEST | ||
| coverage.* | ||
| venv/ | ||
| .coverage | ||
| .cache/ | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -664,7 +664,40 @@ def run_child_validation(self, data): | |
| self.child.initial_data = data | ||
| return super().run_child_validation(data) | ||
| """ | ||
| return self.child.run_validation(data) | ||
| if not hasattr(self.child, 'instance'): | ||
| return self.child.run_validation(data) | ||
|
|
||
| if not ( | ||
| hasattr(self, '_list_serializer_instance_map') and | ||
| isinstance(data, Mapping) | ||
| ): | ||
| return self.child.run_validation(data) | ||
|
|
||
| lookup_field = getattr(getattr(self.child, 'Meta', None), 'lookup_field', 'pk') | ||
| data_pk = data.get(lookup_field) | ||
|
|
||
| if data_pk is None: | ||
| return self.child.run_validation(data) | ||
|
|
||
| child_instance = self._list_serializer_instance_map.get(str(data_pk)) | ||
| if child_instance is None: | ||
| return self.child.run_validation(data) | ||
|
|
||
| original_instance = self.child.instance | ||
| has_initial_data = hasattr(self.child, 'initial_data') | ||
| if has_initial_data: | ||
| original_initial_data = self.child.initial_data | ||
|
|
||
| try: | ||
| self.child.instance = child_instance | ||
| self.child.initial_data = data | ||
| return self.child.run_validation(data) | ||
| finally: | ||
| self.child.instance = original_instance | ||
| if has_initial_data: | ||
| self.child.initial_data = original_initial_data | ||
| elif hasattr(self.child, 'initial_data'): | ||
| delattr(self.child, 'initial_data') | ||
|
|
||
| def to_internal_value(self, data): | ||
| """ | ||
|
|
@@ -702,19 +735,50 @@ def to_internal_value(self, data): | |
| ret = [] | ||
| errors = [] | ||
|
|
||
| for item in data: | ||
| try: | ||
| validated = self.run_child_validation(item) | ||
| except ValidationError as exc: | ||
| errors.append(exc.detail) | ||
| else: | ||
| ret.append(validated) | ||
| errors.append({}) | ||
| # Build a primary key lookup for instance matching in many=True updates. | ||
| instance_map = None | ||
| if self.instance is not None: | ||
| if isinstance(self.instance, Mapping): | ||
| instance_map = {str(k): v for k, v in self.instance.items()} | ||
|
zainnadeem786 marked this conversation as resolved.
|
||
| elif isinstance(self.instance, (list, tuple, models.query.QuerySet)): | ||
| instance_map = {} | ||
|
Comment on lines
+740
to
+744
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @auvipy Thanks for pointing this out. I verified that a related manager is not currently included in the instance-matching path, while The observation looks valid, but given the earlier feedback around keeping the scope focused, I wasn't sure whether manager/queryset parity should be included in this PR or handled separately. Would you prefer support for
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this should be aligned with the changes in this PR. @browniebroke please let me know if otherwise |
||
| lookup_field = getattr(getattr(self.child, 'Meta', None), 'lookup_field', 'pk') | ||
|
|
||
| if any(errors): | ||
| raise ValidationError(errors) | ||
| for obj in self.instance: | ||
| pk = getattr(obj, lookup_field, None) | ||
|
|
||
| return ret | ||
| if pk is not None: | ||
| key = str(pk) | ||
| # If duplicate keys are present, keep the last value, | ||
| # matching standard mapping assignment behavior. | ||
| instance_map[key] = obj | ||
|
zainnadeem786 marked this conversation as resolved.
|
||
|
|
||
| has_instance_map = hasattr(self, '_list_serializer_instance_map') | ||
| if has_instance_map: | ||
| original_instance_map = self._list_serializer_instance_map | ||
|
|
||
| if instance_map is not None: | ||
| self._list_serializer_instance_map = instance_map | ||
|
|
||
| try: | ||
| for item in data: | ||
| try: | ||
| validated = self.run_child_validation(item) | ||
| except ValidationError as exc: | ||
| errors.append(exc.detail) | ||
| else: | ||
| ret.append(validated) | ||
| errors.append({}) | ||
|
|
||
| if any(errors): | ||
| raise ValidationError(errors) | ||
|
|
||
| return ret | ||
| finally: | ||
| if instance_map is not None and has_instance_map: | ||
| self._list_serializer_instance_map = original_instance_map | ||
| elif instance_map is not None and hasattr(self, '_list_serializer_instance_map'): | ||
| delattr(self, '_list_serializer_instance_map') | ||
|
|
||
| def to_representation(self, data): | ||
| """ | ||
|
|
@@ -749,16 +813,27 @@ def save(self, **kwargs): | |
| """ | ||
| Save and return a list of object instances. | ||
| """ | ||
| assert hasattr(self, '_errors'), ( | ||
| 'You must call `.is_valid()` before calling `.save()`.' | ||
| ) | ||
| assert not self.errors, ( | ||
| 'You cannot call `.save()` on a serializer with invalid data.' | ||
| ) | ||
|
zainnadeem786 marked this conversation as resolved.
|
||
|
|
||
| # Guard against incorrect use of `serializer.save(commit=False)` | ||
| assert 'commit' not in kwargs, ( | ||
| "'commit' is not a valid keyword argument to the 'save()' method. " | ||
| "If you need to access data before committing to the database then " | ||
| "inspect 'serializer.validated_data' instead. " | ||
| "You can also pass additional keyword arguments to 'save()' if you " | ||
| "need to set extra attributes on the saved model instance. " | ||
| "For example: 'serializer.save(owner=request.user)'.'" | ||
| "For example: 'serializer.save(owner=request.user)'." | ||
| ) | ||
| assert not hasattr(self, '_data'), ( | ||
| "You cannot call `.save()` after accessing `serializer.data`. " | ||
| "If you need to access data before committing to the database then " | ||
| "inspect 'serializer.validated_data' instead. " | ||
| ) | ||
|
zainnadeem786 marked this conversation as resolved.
|
||
|
|
||
| validated_data = [ | ||
| {**attrs, **kwargs} for attrs in self.validated_data | ||
| ] | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.