Ya hay una gran respuesta de dani herrera , sin embargo, deseo dar más detalles al respecto.
Como se explica en la segunda opción, la solución requerida por el OP es cambiar el diseño e implementar dos restricciones únicas por pares. La analogía con los partidos de baloncesto ilustra el problema de una manera muy práctica.
En lugar de un partido de baloncesto, utilizo ejemplos con juegos de fútbol (o fútbol). Un juego de fútbol (que yo llamo Event
) es jugado por dos equipos (en mis modelos es un equipo Competitor
). Esta es una relación de muchos a muchos ( m:n
), con un n
límite de dos en este caso particular, el principio es adecuado para un número ilimitado.
Así es como se ven nuestros 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
Un evento podría ser:
- título: Copa Carabao, cuarta ronda,
- lugar: Anfield
- hora: 30. octubre 2019, 19:30 GMT
- Participantes:
- nombre: Liverpool, ciudad: Liverpool
- nombre: Arsenal, ciudad: Londres
Ahora tenemos que resolver el problema a partir de la pregunta. Django crea automáticamente una tabla intermedia entre los modelos con una relación de muchos a muchos, pero podemos usar un modelo personalizado y agregar más campos. Yo llamo a ese modelo Participant
:
Participante de clase (modelos.Modelo):
ROLES = (
('H', 'Inicio'),
('V', 'Visitante'),
)
event = models.ForeignKey (Evento, on_delete = models.CASCADE)
competitor = models.ForeignKey (Competitor, on_delete = models.CASCADE)
role = models.CharField (max_length = 1, options = ROLES)
clase Meta:
unique_together = (
('evento', 'rol'),
('evento', 'competidor'),
)
def __str __ (self):
return '{} - {}'. format (self.event, self.get_role_display ())
El ManyToManyField
tiene una opción through
que nos permite especificar el modelo intermedio. Cambiemos eso en el 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
Las restricciones únicas ahora limitarán automáticamente el número de competidores por evento a dos (porque solo hay dos roles: Hogar y Visitante ).
En un evento particular (juego de fútbol) solo puede haber un equipo local y solo un equipo visitante. Un club ( Competitor
) puede aparecer como equipo local o como equipo visitante.
¿Cómo gestionamos ahora todas estas cosas en el administrador? Me gusta esto:
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)
Hemos agregado Participant
como en línea en el EventAdmin
. Cuando creamos nuevos Event
, podemos elegir el equipo local y el equipo visitante. La opción max_num
limita el número de entradas a 2, por lo tanto, no se pueden agregar más de 2 equipos por evento.
Esto se puede refactorizar para diferentes casos de uso. Digamos que nuestros eventos son competiciones de natación y, en lugar de casa y visitante, tenemos carriles 1 a 8. Simplemente refactorizamos 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())
Con esta modificación podemos tener este evento:
Un nadador puede aparecer solo una vez en celo, y un carril puede estar ocupado solo una vez en celo.
Puse el código en GitHub: https://github.com/cezar77/competition .
Una vez más, todos los créditos van a Dani Herrera. Espero que esta respuesta brinde algún valor agregado a los lectores.