Refactor Assignment model to enforce single rota assignment per worker and update related view logic to prevent multiple assignments

This commit is contained in:
Ross
2025-12-19 21:10:43 +00:00
parent f4ab0be347
commit 5cf2480602
3 changed files with 59 additions and 8 deletions
@@ -0,0 +1,16 @@
from django.core.management.base import BaseCommand
from django.db.models import Count
from rota.models import Worker
class Command(BaseCommand):
help = "List workers assigned to more than one rota (useful before migrating to OneToOne Assignment)"
def handle(self, *args, **options):
qs = Worker.objects.annotate(num_rotas=Count('rotas')).filter(num_rotas__gt=1)
if not qs.exists():
self.stdout.write(self.style.SUCCESS('No workers assigned to more than one rota.'))
return
for w in qs:
self.stdout.write(f"Worker id={w.id} name={w.name!r} assigned_rotas={w.num_rotas}")
+7 -2
View File
@@ -297,13 +297,18 @@ class Worker(models.Model):
class Assignment(models.Model):
"""Join model assigning a worker to a rota (can hold role / metadata)."""
worker = models.ForeignKey(Worker, on_delete=models.CASCADE)
# Enforce that a Worker may only be assigned to a single RotaSchedule by
# using a OneToOneField here. This ensures at the DB level that each
# Worker has at most one Assignment (and therefore at most one rota).
worker = models.OneToOneField(Worker, on_delete=models.CASCADE)
rota = models.ForeignKey(RotaSchedule, on_delete=models.CASCADE)
role = models.CharField(max_length=100, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
unique_together = ("worker", "rota")
# With OneToOneField on worker, uniqueness for (worker, rota) is
# implied by the worker uniqueness. Keep ordering for convenience.
ordering = ("-created_at",)
def __str__(self):
return f"{self.worker} -> {self.rota}"
+36 -6
View File
@@ -334,12 +334,19 @@ def worker_create(request):
# If we created a worker and a rota_id was provided, create Assignment
if rota is not None:
from .models import Assignment
# Only assign if the worker is not already assigned to any rota.
try:
Assignment.objects.create(worker=worker, rota=rota)
if not worker.rotas.exists():
Assignment.objects.create(worker=worker, rota=rota)
else:
# skip assigning; worker already assigned elsewhere
pass
except Exception:
# best-effort: try using the M2M add as fallback
# best-effort: try using the M2M add as fallback but still
# respect single-rota constraint
try:
rota.workers.add(worker)
if not worker.rotas.exists():
rota.workers.add(worker)
except Exception:
pass
@@ -454,14 +461,37 @@ def worker_import(request, rota_id=None):
except Exception:
fte = 1.0
w = Worker.objects.create(name=name.strip(), email=email.strip(), site=site.strip(), grade=grade, fte=fte)
# If an email is provided, try to find an existing worker to
# avoid duplicates; otherwise create a new record.
existing = None
if email:
try:
existing = Worker.objects.filter(email__iexact=email.strip()).first()
except Exception:
existing = None
if existing is not None:
w = existing
else:
w = Worker.objects.create(name=name.strip(), email=email.strip(), site=site.strip(), grade=grade, fte=fte)
created.append(w)
# Assignment: only assign if the worker is not already assigned
# to another rota. If already assigned, record an error and
# skip assignment.
if assign_to_rota and rota is not None:
try:
Assignment.objects.create(worker=w, rota=rota)
if not w.rotas.exists():
Assignment.objects.create(worker=w, rota=rota)
else:
# If already assigned to this rota, nothing to do.
assigned_rota = w.rotas.first()
if assigned_rota and assigned_rota.pk != rota.pk:
errors.append(f"Row {i+1}: worker '{w.name}' already assigned to rota '{assigned_rota.name}'")
except Exception:
try:
rota.workers.add(w)
if not w.rotas.exists():
rota.workers.add(w)
except Exception:
pass
except Exception as exc: