feat: add group management for workers and shifts, including validation for overlapping groups
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user