diff --git a/partial_index/mixins.py b/partial_index/mixins.py index c130d28..75785a1 100644 --- a/partial_index/mixins.py +++ b/partial_index/mixins.py @@ -1,3 +1,4 @@ +from collections import defaultdict from django.core.exceptions import ImproperlyConfigured, ValidationError, NON_FIELD_ERRORS from django.db.models import Q @@ -73,6 +74,7 @@ def validate_partial_unique(self): if unique_idxs: model_fields = set(f.name for f in self._meta.get_fields(include_parents=True, include_hidden=True)) + errors = defaultdict(list) for idx in unique_idxs: where = idx.where if not isinstance(where, Q): @@ -89,16 +91,20 @@ def validate_partial_unique(self): 'This is a bug in the PartialIndex definition or the django-partial-index library itself.') values = {} + skip = False for field_name in mentioned_fields: field_value = getattr(self, field_name) if field_value is None and field_name in idx.fields: # Can never be unique if value is NULL. If # field is non-nullable we'll get a validation # error from the field validations themselves. - return + skip = True else: values[field_name] = field_value + if skip: + continue + conflict = self.__class__.objects.filter(**values) # Step 1 and 3 conflict = conflict.filter(where) # Step 2 if self.pk: @@ -109,4 +115,7 @@ def validate_partial_unique(self): key = idx.fields[0] else: key = NON_FIELD_ERRORS - raise PartialUniqueValidationError({key: self.unique_error_message(self.__class__, sorted(idx.fields))}) + errors[key].append(self.unique_error_message(self.__class__, sorted(idx.fields))) + + if errors: + raise PartialUniqueValidationError(errors) diff --git a/tests/test_models.py b/tests/test_models.py index c91a847..f04f348 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -216,7 +216,7 @@ def test_nullable_roomnumber_q_same_no_conflict_for_null_number(self): NullableRoomNumberQ.objects.create(room=self.room1, room_number=None) -class PartialIndexLabelValidationTest(TestCase): +class PartialIndexLabelValidationTest(TransactionTestCase): """Test that partial unique validations are all executed.""" def setUp(self): @@ -267,3 +267,20 @@ def test_standard_unique_together_constraints_do_not_block_evaluation_of_partial with self.assertRaises(IntegrityError): label.save() + + def test_all_partial_constraints_are_included_in_validation_errors(self): + Label.objects.create(label='a', user=self.user1, room=self.room1, uuid='11111111-0000-0000-0000-000000000000', created_at='2019-01-01T11:11:11') + + label = Label(label='a', user=self.user1, room=self.room1, uuid='22222222-0000-0000-0000-000000000000', created_at='2019-01-02T22:22:22') + with self.assertRaises(ValidationError) as cm: + label.full_clean() + + self.assertSetEqual({NON_FIELD_ERRORS}, set(cm.exception.message_dict.keys())) + self.assertEqual(2, len(cm.exception.error_dict[NON_FIELD_ERRORS])) + self.assertEqual('unique_together', cm.exception.error_dict[NON_FIELD_ERRORS][0].code) + self.assertEqual(['label', 'room'], cm.exception.error_dict[NON_FIELD_ERRORS][0].params['unique_check']) + self.assertEqual('unique_together', cm.exception.error_dict[NON_FIELD_ERRORS][1].code) + self.assertEqual(['label', 'user'], cm.exception.error_dict[NON_FIELD_ERRORS][1].params['unique_check']) + + with self.assertRaises(IntegrityError): + label.save()