feat: add group management for workers and shifts, including validation for overlapping groups

This commit is contained in:
Ross
2026-07-09 22:14:30 +01:00
parent 33f50c7278
commit b5525bd3fb
6 changed files with 259 additions and 24 deletions
+32
View File
@@ -46,6 +46,7 @@ class WorkerForm(forms.ModelForm):
assign_as_block_preferences = forms.CharField(required=False, widget=forms.Textarea, help_text="JSON mapping shift_name->weight", label="Block preferences (JSON)")
shift_fte_overrides = forms.CharField(required=False, widget=forms.Textarea, help_text="JSON mapping shift_name->fte", label="Shift FTE overrides (JSON)")
exact_shifts = forms.CharField(required=False, widget=forms.Textarea, help_text="JSON mapping shift_name->exact_count", label="Exact shifts (JSON)")
groups = forms.CharField(required=False, widget=forms.Textarea, help_text="JSON list of group names (e.g. [\"group1\", \"group2\"])", label="Groups (JSON)")
previous_shifts = forms.CharField(required=False, widget=forms.Textarea, help_text="Free JSON structure for previous shifts", label="Previous shifts (JSON)")
shift_balance_extra = forms.CharField(required=False, widget=forms.Textarea, help_text="JSON structure for extra shift-balance penalties", label="Shift balance extra (JSON)")
allowed_multi_shift_sets = forms.CharField(required=False, widget=forms.Textarea, help_text="JSON list of sets (e.g. [[\"a\",\"b\"], ...])", label="Allowed multi-shift sets (JSON)")
@@ -96,6 +97,7 @@ class WorkerForm(forms.ModelForm):
"assign_as_block_preferences",
"shift_fte_overrides",
"exact_shifts",
"groups",
"previous_shifts",
"shift_balance_extra",
"allowed_multi_shift_sets",
@@ -149,6 +151,7 @@ class WorkerForm(forms.ModelForm):
"assign_as_block_preferences",
"shift_fte_overrides",
"exact_shifts",
"groups",
"previous_shifts",
"shift_balance_extra",
"allowed_multi_shift_sets",
@@ -800,6 +803,16 @@ class ShiftForm(forms.Form):
widget=forms.Textarea(attrs={"rows": 4, "class": "textarea"}),
help_text="Optional JSON list of constraint objects for this shift (e.g. [{'name':'night','options':{}}])",
)
include_groups = forms.CharField(
required=False,
help_text="Comma separated list of groups that can work this shift (leave empty for all)",
label="Include groups",
)
exclude_groups = forms.CharField(
required=False,
help_text="Comma separated list of groups that cannot work this shift",
label="Exclude groups",
)
def clean_sites(self):
val = self.cleaned_data["sites"]
@@ -841,3 +854,22 @@ class ShiftForm(forms.Form):
raise
except Exception as exc:
raise forms.ValidationError(f"Invalid JSON for constraints: {exc}")
def clean_include_groups(self):
val = self.cleaned_data.get("include_groups") or ""
return [g.strip() for g in val.split(",") if g.strip()]
def clean_exclude_groups(self):
val = self.cleaned_data.get("exclude_groups") or ""
return [g.strip() for g in val.split(",") if g.strip()]
def clean(self):
cleaned_data = super().clean()
include = cleaned_data.get("include_groups") or []
exclude = cleaned_data.get("exclude_groups") or []
overlap = set(include).intersection(set(exclude))
if overlap:
raise forms.ValidationError(
f"Include groups and Exclude groups cannot overlap. Overlapping groups: {', '.join(overlap)}"
)
return cleaned_data
+11
View File
@@ -274,6 +274,17 @@ class Worker(models.Model):
else:
pyd_kwargs[k] = []
set_keys = ["groups"]
for k in set_keys:
v = pyd_kwargs.get(k, None)
try:
if v is None:
pyd_kwargs[k] = set()
else:
pyd_kwargs[k] = set(v)
except Exception:
pyd_kwargs[k] = set()
# allowed_multi_shift_sets is expected to be a list of sets; if the
# DB contains lists-of-lists convert inner lists to sets.
ams = pyd_kwargs.get("allowed_multi_shift_sets")
+7
View File
@@ -1690,6 +1690,8 @@ def shift_add(request, rota_id):
"workers_required": int(data["workers_required"]),
"assign_as_block": bool(data["assign_as_block"]),
"balance_offset": data.get("balance_offset"),
"include_groups": data.get("include_groups", []),
"exclude_groups": data.get("exclude_groups", []),
"constraints": data.get("constraints", []),
}
@@ -1738,6 +1740,9 @@ def shift_edit(request, rota_id, idx):
"days": data["days"],
"workers_required": int(data["workers_required"]),
"assign_as_block": bool(data["assign_as_block"]),
"balance_offset": data.get("balance_offset"),
"include_groups": data.get("include_groups", []),
"exclude_groups": data.get("exclude_groups", []),
"constraints": data.get("constraints", []),
}
current = rota.shifts or []
@@ -1766,6 +1771,8 @@ def shift_edit(request, rota_id, idx):
"workers_required": shift.get("workers_required"),
"assign_as_block": shift.get("assign_as_block", False),
"balance_offset": shift.get("balance_offset", None),
"include_groups": ",".join(shift.get("include_groups") or []),
"exclude_groups": ",".join(shift.get("exclude_groups") or []),
"constraints": json.dumps(shift.get("constraints", [])) if shift.get("constraints") is not None else "",
}
form = ShiftForm(initial=initial)