Já existe uma ótima resposta de dani herrera , mas desejo aprofundar a questão.
Conforme explicado na segunda opção, a solução, conforme exigido pelo OP, é alterar o design e implementar duas restrições exclusivas aos pares. A analogia com as partidas de basquete ilustra o problema de uma maneira muito prática.
Em vez de uma partida de basquete, uso exemplo em jogos de futebol (ou futebol). Um jogo de futebol (como eu chamo Event) é jogado por duas equipes (nos meus modelos, uma equipe Competitor). Essa é uma relação de muitos para muitos ( m:n), com nlimitado a dois nesse caso específico, o princípio é adequado para um número ilimitado.
Aqui está a aparência dos nossos modelos:
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
Um evento pode ser:
- título: Taça Carabao, 4ª rodada,
- local do evento: Anfield
- hora: 30. outubro 2019, 19:30 GMT
- participantes:
- nome: Liverpool, cidade: Liverpool
- nome: Arsenal, cidade: Londres
Agora temos que resolver o problema da questão. O Django cria automaticamente uma tabela intermediária entre os modelos com uma relação de muitos para muitos, mas podemos usar um modelo personalizado e adicionar mais campos. Eu chamo esse modelo Participant:
classe Participant (models.Model):
ROLES = (
('H', 'Casa'),
('V', 'Visitante'),
)
event = models.ForeignKey (Evento, on_delete = models.CASCADE)
concorrente = models.ForeignKey (Concorrente, on_delete = models.CASCADE)
role = models.CharField (max_length = 1, escolhas = ROLES)
classe Meta:
unique_together = (
('evento', 'papel'),
('evento', 'concorrente'),
)
def __str __ (próprio):
retornar '{} - {}'. formato (self.event, self.get_role_display ())
O ManyToManyFieldpossui uma opção throughque nos permite especificar o modelo intermediário. Vamos mudar isso no modelo 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
As restrições exclusivas agora limitarão automaticamente o número de competidores por evento a dois (porque existem apenas duas funções: Casa e Visitante ).
Em um evento específico (jogo de futebol), pode haver apenas um time em casa e apenas um time de visitantes. Um clube ( Competitor) pode aparecer como time da casa ou como visitante.
Como gerenciamos agora todas essas coisas no administrador? Como isso:
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)
Adicionamos o Participantinline no EventAdmin. Quando criamos novos Event, podemos escolher a equipe da casa e a equipe do visitante. A opção max_numlimita o número de entradas a 2, portanto, não é possível adicionar mais de 2 equipes por evento.
Isso pode ser refatorado para diferentes casos de uso. Digamos que nossos eventos são competições de natação e, em vez de casa e visitante, temos as faixas de 1 a 8. Apenas refatoramos o 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())
Com esta modificação, podemos ter este evento:
Um nadador pode aparecer apenas uma vez no calor e uma pista pode ser ocupada apenas uma vez no calor.
Coloquei o código no GitHub: https://github.com/cezar77/competition .
Mais uma vez, todos os créditos vão para dani herrera. Espero que esta resposta ofereça algum valor agregado aos leitores.