|
13 | 13 | import copy
|
14 | 14 | import inspect
|
15 | 15 | import traceback
|
16 |
| -from collections import OrderedDict |
| 16 | +from collections import OrderedDict, defaultdict |
17 | 17 | from collections.abc import Mapping
|
18 | 18 |
|
19 | 19 | from django.core.exceptions import FieldDoesNotExist, ImproperlyConfigured
|
@@ -1508,28 +1508,55 @@ def get_unique_together_validators(self):
|
1508 | 1508 | # which may map onto a model field. Any dotted field name lookups
|
1509 | 1509 | # cannot map to a field, and must be a traversal, so we're not
|
1510 | 1510 | # including those.
|
1511 |
| - field_names = { |
1512 |
| - field.source for field in self._writable_fields |
| 1511 | + field_sources = OrderedDict( |
| 1512 | + (field.field_name, field.source) for field in self._writable_fields |
1513 | 1513 | if (field.source != '*') and ('.' not in field.source)
|
1514 |
| - } |
| 1514 | + ) |
1515 | 1515 |
|
1516 | 1516 | # Special Case: Add read_only fields with defaults.
|
1517 |
| - field_names |= { |
1518 |
| - field.source for field in self.fields.values() |
| 1517 | + field_sources.update(OrderedDict( |
| 1518 | + (field.field_name, field.source) for field in self.fields.values() |
1519 | 1519 | if (field.read_only) and (field.default != empty) and (field.source != '*') and ('.' not in field.source)
|
1520 |
| - } |
| 1520 | + )) |
| 1521 | + |
| 1522 | + # Invert so we can find the serializer field names that correspond to |
| 1523 | + # the model field names in the unique_together sets. This also allows |
| 1524 | + # us to check that multiple fields don't map to the same source. |
| 1525 | + source_map = defaultdict(list) |
| 1526 | + for name, source in field_sources.items(): |
| 1527 | + source_map[source].append(name) |
1521 | 1528 |
|
1522 | 1529 | # Note that we make sure to check `unique_together` both on the
|
1523 | 1530 | # base model class, but also on any parent classes.
|
1524 | 1531 | validators = []
|
1525 | 1532 | for parent_class in model_class_inheritance_tree:
|
1526 | 1533 | for unique_together in parent_class._meta.unique_together:
|
1527 |
| - if field_names.issuperset(set(unique_together)): |
1528 |
| - validator = UniqueTogetherValidator( |
1529 |
| - queryset=parent_class._default_manager, |
1530 |
| - fields=unique_together |
| 1534 | + # Skip if serializer does not map to all unique together sources |
| 1535 | + if not set(source_map).issuperset(set(unique_together)): |
| 1536 | + continue |
| 1537 | + |
| 1538 | + for source in unique_together: |
| 1539 | + assert len(source_map[source]) == 1, ( |
| 1540 | + "Unable to create `UniqueTogetherValidator` for " |
| 1541 | + "`{model}.{field}` as `{serializer}` has multiple " |
| 1542 | + "fields ({fields}) that map to this model field. " |
| 1543 | + "Either remove the extra fields, or override " |
| 1544 | + "`Meta.validators` with a `UniqueTogetherValidator` " |
| 1545 | + "using the desired field names." |
| 1546 | + .format( |
| 1547 | + model=self.Meta.model.__name__, |
| 1548 | + serializer=self.__class__.__name__, |
| 1549 | + field=source, |
| 1550 | + fields=', '.join(source_map[source]), |
| 1551 | + ) |
1531 | 1552 | )
|
1532 |
| - validators.append(validator) |
| 1553 | + |
| 1554 | + field_names = tuple(source_map[f][0] for f in unique_together) |
| 1555 | + validator = UniqueTogetherValidator( |
| 1556 | + queryset=parent_class._default_manager, |
| 1557 | + fields=field_names |
| 1558 | + ) |
| 1559 | + validators.append(validator) |
1533 | 1560 | return validators
|
1534 | 1561 |
|
1535 | 1562 | def get_unique_for_date_validators(self):
|
|
0 commit comments