Refactor Assignment model to enforce single rota assignment per worker and update related view logic to prevent multiple assignments
This commit is contained in:
@@ -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}")
|
||||||
@@ -297,13 +297,18 @@ class Worker(models.Model):
|
|||||||
class Assignment(models.Model):
|
class Assignment(models.Model):
|
||||||
"""Join model assigning a worker to a rota (can hold role / metadata)."""
|
"""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)
|
rota = models.ForeignKey(RotaSchedule, on_delete=models.CASCADE)
|
||||||
role = models.CharField(max_length=100, blank=True)
|
role = models.CharField(max_length=100, blank=True)
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
class Meta:
|
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):
|
def __str__(self):
|
||||||
return f"{self.worker} -> {self.rota}"
|
return f"{self.worker} -> {self.rota}"
|
||||||
|
|||||||
@@ -334,11 +334,18 @@ def worker_create(request):
|
|||||||
# If we created a worker and a rota_id was provided, create Assignment
|
# If we created a worker and a rota_id was provided, create Assignment
|
||||||
if rota is not None:
|
if rota is not None:
|
||||||
from .models import Assignment
|
from .models import Assignment
|
||||||
|
# Only assign if the worker is not already assigned to any rota.
|
||||||
try:
|
try:
|
||||||
|
if not worker.rotas.exists():
|
||||||
Assignment.objects.create(worker=worker, rota=rota)
|
Assignment.objects.create(worker=worker, rota=rota)
|
||||||
|
else:
|
||||||
|
# skip assigning; worker already assigned elsewhere
|
||||||
|
pass
|
||||||
except Exception:
|
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:
|
try:
|
||||||
|
if not worker.rotas.exists():
|
||||||
rota.workers.add(worker)
|
rota.workers.add(worker)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
@@ -454,13 +461,36 @@ def worker_import(request, rota_id=None):
|
|||||||
except Exception:
|
except Exception:
|
||||||
fte = 1.0
|
fte = 1.0
|
||||||
|
|
||||||
|
# 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)
|
w = Worker.objects.create(name=name.strip(), email=email.strip(), site=site.strip(), grade=grade, fte=fte)
|
||||||
created.append(w)
|
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:
|
if assign_to_rota and rota is not None:
|
||||||
try:
|
try:
|
||||||
|
if not w.rotas.exists():
|
||||||
Assignment.objects.create(worker=w, rota=rota)
|
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:
|
except Exception:
|
||||||
try:
|
try:
|
||||||
|
if not w.rotas.exists():
|
||||||
rota.workers.add(w)
|
rota.workers.add(w)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|||||||
Reference in New Issue
Block a user