There is already a great answer from dani herrera, however I wish to further elaborate on it.
As explained in the second option, the solution as required by the OP is to change the design and implement two unique constraints pairwise. The analogy with the basketball matches illustrates the problem in a very practical way.
Instead of a basketball match, I use example with football (or soccer) games. A football game (which I call it Event
) is played by two teams (in my models a team is Competitor
). This is a many-to-many relation (m:n
), with n
limited to two in this particular case, the principle is suitable for an unlimited number.
Here is how our models look:
class Competitor(models.Model):
name = models.CharField(max_length=100)
city = models.CharField(max_length=100)
def __str__(self):
return self.name
class Event(models.Model):
title = models.CharField(max_length=200)
venue = models.CharField(max_length=100)
time = models.DateTimeField()
participants = models.ManyToManyField(Competitor)
def __str__(self):
return self.title
An event could be:
- title: Carabao Cup, 4th round,
- venue: Anfield
- time: 30. October 2019, 19:30 GMT
- participants:
- name: Liverpool, city: Liverpool
- name: Arsenal, city: London
Now we have to solve the issue from the question. Django automatically creates an intermediate table between the models with a many-to-many relation, but we can use a custom model and add further fields. I call that model Participant
:
class Participant(models.Model):
ROLES = (
('H', 'Home'),
('V', 'Visitor'),
)
event = models.ForeignKey(Event, on_delete=models.CASCADE)
competitor = models.ForeignKey(Competitor, on_delete=models.CASCADE)
role = models.CharField(max_length=1, choices=ROLES)
class Meta:
unique_together = (
('event', 'role'),
('event', 'competitor'),
)
def __str__(self):
return '{} - {}'.format(self.event, self.get_role_display())
The ManyToManyField
has an option through
that allows us to specify the intermediate model. Let's change that in the model Event
:
class Event(models.Model):
title = models.CharField(max_length=200)
venue = models.CharField(max_length=100)
time = models.DateTimeField()
participants = models.ManyToManyField(
Competitor,
related_name='events', # if we want to retrieve events for a competitor
through='Participant'
)
def __str__(self):
return self.title
The unique constraints now will automatically limit the number of competitors per event to two (because there are only two roles: Home and Visitor).
In a particular event (football game) there can be only one home team and only one visitor team. A club (Competitor
) can appear either as home team or as visitor team.
How do we manage now all these things in the admin? Like this:
from django.contrib import admin
from .models import Competitor, Event, Participant
class ParticipantInline(admin.StackedInline): # or admin.TabularInline
model = Participant
max_num = 2
class CompetitorAdmin(admin.ModelAdmin):
fields = ('name', 'city',)
class EventAdmin(admin.ModelAdmin):
fields = ('title', 'venue', 'time',)
inlines = [ParticipantInline]
admin.site.register(Competitor, CompetitorAdmin)
admin.site.register(Event, EventAdmin)
We have added the Participant
as inline in the EventAdmin
. When we create new Event
we can choose the home team and the visitor team. The option max_num
limits the number of entries to 2, therefore no more then 2 teams can be added per event.
This can be refactored for a different use cases. Let's say our events are swimming competitions and instead of home and visitor, we have lanes 1 to 8. We just refactor the Participant
:
class Participant(models.Model):
ROLES = (
('L1', 'lane 1'),
('L2', 'lane 2'),
# ... L3 to L8
)
event = models.ForeignKey(Event, on_delete=models.CASCADE)
competitor = models.ForeignKey(Competitor, on_delete=models.CASCADE)
role = models.CharField(max_length=1, choices=ROLES)
class Meta:
unique_together = (
('event', 'role'),
('event', 'competitor'),
)
def __str__(self):
return '{} - {}'.format(self.event, self.get_role_display())
With this modification we can have this event:
A swimmer can appear only once in a heat, and a lane can be occupied only once in a heat.
I put the code to GitHub: https://github.com/cezar77/competition.
Again, all credits go to dani herrera. I hope this answer provides some added value to the readers.