Reputation: 3527
I have two models like so:
class Group(...):
pass
class Identifier(...):
value = models.CharField(...)
group = models.ForeignKey('Group', ..., related_named = 'identifiers')
How can I:
Group
to only have at most 4 Identifiers
?Identifiers
(the value of the identifier) is unique across all Groups
?For part 2, here is an example of the flattened Groups
table:
row | id__0__val | id__1__val | id__2__val | id__3__val
--- | ---------- | ---------- | ---------- | ----------
0 | abc | 123 | xyz | 456
1 | abc | 123 | xyz | - <-- valid (nulls are okay)
2 | 123 | abc | xyz | 456 <-- invalid (same combo as row 0)
Previously I have tried (something like) this, but it seems messy, has limited functionality, and I'm not sure it will even work:
class Group(...):
id__0 = models.OneToOneField('Identifier', blank = True, null = True, ...)
id__1 = models.OneToOneField('Identifier', blank = True, null = True, ...)
id__2 = models.OneToOneField('Identifier', blank = True, null = True, ...)
id__3 = models.OneToOneField('Identifier', blank = True, null = True, ...)
class Meta:
unique_together = ('id__0__value', 'id__1__value', 'id__2__value', 'id__3__value')
What is a better way to handle this constraint?
Upvotes: 4
Views: 734
Reputation: 12078
My take on this, but doing it through a clean
method:
class Group(models.Model):
MAX_IDENTIFIERS_PER_GROUP = 4
class Identifier(models.Model):
value = models.CharField()
group = models.ForeignKey('Group', related_named='identifiers')
def clean(self):
identifiers = self.group.identifiers.values_list('value', flat=True)
# If a new identifier is being created, but the current group already has identifiers == MAX_IDENTIFIERS_PER_GROUP
if not self.pk and len(identifiers) == Group.MAX_IDENTIFIERS_PER_GROUP:
raise ValidationError(f'Cannot have more than {Group.MAX_IDENTIFIERS_PER_GROUP} identifiers for group {self.group.pk}')
# If there is another group with the same values in identifiers as the current identifier's group
conflicting_identifier = Identifier.objects
.filter(value__in=set(identifiers + [self.value, ]))
.exclude(group=self.group)
.values('group')
.annotate(value_count=Count('value'))
.filter(value_count__gte=Group.MAX_IDENTIFIERS_PER_GROUP).first()
if conflicting_identifier:
raise ValidationError(f'Current identifier combination is already present in group {conflicting_identifier.group.pk}')
Note that the clean
does not get run automatically in save
so this needs to be manually used. (Haven't tested this but the gist should be clear ;) )
Upvotes: 1
Reputation: 11931
Could do it via validate_unique method:
class Group(models.Model):
...
def validate_unique(self, exclude=None):
qs = Identifier.objects.filter(group_id=self.id)
# restrict a Group to only have at most 4 Identifiers
if qs.count() >= 4:
raise ValidationError("group already has 4 Identifiers")
# Ensure that any combination of up to 4 Identifiers is unique across all Groups
values = []
for group in Group.objects.all():
values.append([i.value for i in group.identifiers])
current_values = [i.value for i in self.identifiers]
# https://stackoverflow.com/questions/22483730/python-check-if-list-of-lists-of-lists-contains-a-specific-list
if current_values in values:
raise ValidationError("group with these identifiers already exists")
This is pseudo code so might not work - but you get the idea :)
Upvotes: 0