Skip to content

Commit

Permalink
Do not abort early when standard uniqueness validations fail
Browse files Browse the repository at this point in the history
Just like Django will do in the standard behaviour, we want to ensure
all errors are included.  Therefore, we catch validation errors raised
by Django and merge these with our own.
  • Loading branch information
Peter Bex committed Jul 22, 2019
1 parent a5d954e commit 2cb9cd5
Show file tree
Hide file tree
Showing 3 changed files with 86 additions and 3 deletions.
17 changes: 15 additions & 2 deletions partial_index/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,22 @@ class MyModel(ValidatePartialUniqueMixin, models.Model):
"""

def validate_unique(self, exclude=None):
errors = {}

# Standard unique validation first.
super(ValidatePartialUniqueMixin, self).validate_unique(exclude=exclude)
self.validate_partial_unique()
try:
super(ValidatePartialUniqueMixin, self).validate_unique(exclude=exclude)
except ValidationError as e:
errors.update(e.error_dict)

# Merge ours into the existing errors (if any)
try:
self.validate_partial_unique()
except ValidationError as e:
errors.update(e.error_dict)

if errors:
raise PartialUniqueValidationError(errors)

def validate_partial_unique(self):
"""Check partial unique constraints on the model and raise ValidationError if any failed.
Expand Down
55 changes: 54 additions & 1 deletion tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from django.utils import timezone
from django.core.exceptions import NON_FIELD_ERRORS, ValidationError

from testapp.models import User, Room, RoomBookingText, JobText, ComparisonText, RoomBookingQ, JobQ, ComparisonQ
from testapp.models import User, Room, RoomBookingText, JobText, ComparisonText, RoomBookingQ, JobQ, ComparisonQ, Label


class PartialIndexRoomBookingTest(TransactionTestCase):
Expand Down Expand Up @@ -154,3 +154,56 @@ def test_comparison_text_duplicate_different_numbers(self):
def test_comparison_q_duplicate_different_numbers(self):
ComparisonQ.objects.create(a=1, b=2)
ComparisonQ.objects.create(a=1, b=2)


class PartialIndexLabelValidationTest(TransactionTestCase):
"""Test that partial unique validations are all executed."""

def setUp(self):
self.room1 = Room.objects.create(name='room 1')
self.room2 = Room.objects.create(name='room 2')
self.user1 = User.objects.create(name='user 1')
self.user2 = User.objects.create(name='user 2')

def test_single_unique_constraints_are_still_evaluated(self):
Label.objects.create(label='a', user=self.user1, room=self.room1, uuid='11111111-0000-0000-0000-000000000000', created_at='2019-01-01T00:00:00')

label = Label(label='b', user=self.user2, room=self.room2, uuid='22222222-0000-0000-0000-000000000000', created_at='2019-01-01T00:00:00')
with self.assertRaises(ValidationError) as cm:
label.full_clean()

self.assertSetEqual({'created_at'}, set(cm.exception.message_dict.keys()))
self.assertEqual('unique', cm.exception.error_dict['created_at'][0].code)

with self.assertRaises(IntegrityError):
label.save()

def test_standard_single_field_unique_constraints_do_not_block_evaluation_of_partial_index_constraints(self):
Label.objects.create(label='a', user=self.user1, room=self.room1, uuid='11111111-0000-0000-0000-000000000000', created_at='2019-01-01T00:00:00')

label = Label(label='b', user=self.user2, room=self.room2, uuid='11111111-0000-0000-0000-000000000000', created_at='2019-01-01T00:00:00')
with self.assertRaises(ValidationError) as cm:
label.full_clean()

self.assertSetEqual({'created_at', 'uuid'}, set(cm.exception.message_dict.keys()))
self.assertEqual('unique', cm.exception.error_dict['created_at'][0].code)
self.assertEqual('unique', cm.exception.error_dict['uuid'][0].code)

with self.assertRaises(IntegrityError):
label.save()

def test_standard_unique_together_constraints_do_not_block_evaluation_of_partial_index_constraints(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='b', user=self.user1, room=self.room1, uuid='11111111-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, 'uuid'}, set(cm.exception.message_dict.keys()))
self.assertEqual(1, len(cm.exception.error_dict['uuid']))
self.assertEqual(1, len(cm.exception.error_dict[NON_FIELD_ERRORS]))
self.assertEqual('unique', cm.exception.error_dict['uuid'][0].code)
self.assertEqual('unique_together', cm.exception.error_dict[NON_FIELD_ERRORS][0].code)

with self.assertRaises(IntegrityError):
label.save()
17 changes: 17 additions & 0 deletions tests/testapp/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,20 @@ class Meta:
indexes = [
PartialIndex(fields=['a', 'b'], unique=True, where=PQ(a=PF('b'))),
]


class Label(ValidatePartialUniqueMixin, models.Model):
room = models.ForeignKey(Room, on_delete=models.CASCADE, null=True, blank=True)
user = models.ForeignKey(User, on_delete=models.CASCADE, null=True, blank=True)
label = models.TextField()
uuid = models.UUIDField()
created_at = models.DateTimeField(unique=True)
deleted_at = models.DateTimeField(null=True, blank=True)

class Meta:
indexes = [
PartialIndex(fields=['room', 'label'], unique=True, where=PQ(deleted_at__isnull=True)),
PartialIndex(fields=['user', 'label'], unique=True, where=PQ(deleted_at__isnull=True)),
PartialIndex(fields=['uuid'], unique=True, where=PQ(deleted_at__isnull=True)),
]
unique_together = [['room', 'user']] # Regardless of deletion status

0 comments on commit 2cb9cd5

Please sign in to comment.