Add rota-builder summary to detail view with weeks, shifts, and workers count

This commit is contained in:
Ross
2025-12-17 17:25:22 +00:00
parent 8458279e19
commit 023e345591
4 changed files with 140 additions and 3 deletions
@@ -11,6 +11,19 @@
<p class="subtitle">{{ rota.description }}</p>
<p>Period: {{ rota.start_date }} → {{ rota.end_date }}</p>
<div class="box" style="margin-top:0.75rem;">
<h2 class="subtitle is-6">RotaBuilder summary</h2>
<div class="columns is-mobile is-multiline" style="margin-bottom:0.5rem;">
<div class="column is-narrow"><strong>Weeks</strong></div>
<div class="column">{{ weeks_to_rota|default:'—' }}</div>
<div class="column is-narrow"><strong>Shifts</strong></div>
<div class="column">{{ shift_count|default:'—' }}</div>
<div class="column is-narrow"><strong>Workers</strong></div>
<div class="column">{{ worker_count|default:'—' }}</div>
</div>
{# constraints sample intentionally omitted here; keep summary compact #}
</div>
<div class="mb-4">
<p>
<button class="button is-link" hx-get="{% url 'rota:worker_add' %}?rota_id={{ rota.id }}" hx-target="#modal" hx-swap="innerHTML">Add worker</button>
+71
View File
@@ -441,6 +441,77 @@ class RotaScheduleForm(forms.ModelForm):
self.fields["end_date"].initial = end_date
return cleaned
def save(self, commit=True):
"""Save the RotaSchedule and persist dynamic `opt__*` fields into
`instance.options`.
This persists both typed constraint option fields (created from
RotaConstraintOptions) and the small set of builder-argument fields
(created in __init__). For structured fields we attempt to parse JSON
textareas into Python objects.
"""
instance = super().save(commit=False)
opts = instance.options or {}
# Persist typed constraint option fields using inferred defaults map
try:
defaults = getattr(self, "_constraint_defaults", {}) or {}
except Exception:
defaults = {}
for key, default in defaults.items():
field_name = f"opt__{key}"
if field_name in self.cleaned_data:
val = self.cleaned_data[field_name]
if isinstance(default, (list, dict)):
# JSON textarea
try:
parsed = json.loads(val) if val is not None and val != "" else []
except Exception:
parsed = val
opts[key] = parsed
else:
if val == "" or val is None:
opts[key] = None
else:
opts[key] = val
# Persist any remaining opt__ fields (builder args and others)
for fname, val in self.cleaned_data.items():
if not fname.startswith("opt__"):
continue
key = fname[5:]
if key in defaults:
# already handled
continue
v = val
# Try to parse JSON-like strings into structures
if isinstance(v, str):
s = v.strip()
if s.startswith("[") or s.startswith("{"):
try:
v = json.loads(v)
except Exception:
pass
opts[key] = v
# Persist the top-level 'weeks' value from the form into options so
# the builder can prefer this explicit value instead of deriving it
# from end_date. Do not persist end_date itself here.
try:
weeks_val = self.cleaned_data.get("weeks")
if weeks_val is not None:
opts["weeks"] = int(weeks_val)
except Exception:
pass
instance.options = opts
if commit:
instance.save()
return instance
class WorkerSelfServiceForm(forms.ModelForm):
"""Small form exposed to workers via tokenized links so they can set
+22 -2
View File
@@ -55,14 +55,34 @@ class RotaSchedule(models.Model):
)
# compute weeks_to_rota (integer number of weeks)
delta = self.end_date - self.start_date
weeks_to_rota = max(1, int(delta.days // 7))
# Prefer an explicit value stored in `self.options['weeks']` (set by
# the rota edit form). If absent, fall back to deriving from the
# stored end_date (backwards compatible).
opts_preview = dict(self.options or {})
weeks_opt = None
if "weeks" in opts_preview:
try:
weeks_opt = int(opts_preview.get("weeks"))
except Exception:
weeks_opt = None
if weeks_opt is not None:
weeks_to_rota = max(1, weeks_opt)
else:
delta = self.end_date - self.start_date
weeks_to_rota = max(1, int(delta.days // 7))
# Build kwargs for RotaBuilder from any explicit builder args stored in
# `self.options` (kept for backward compatibility), and treat the
# remaining keys as constraint options which should map to
# `RotaConstraintOptions`.
opts = dict(self.options or {})
# Remove 'weeks' from opts before handing to constraint options so
# it doesn't cause validation failures in RotaConstraintOptions.
if "weeks" in opts:
try:
opts.pop("weeks")
except Exception:
pass
builder_keys = [
"balance_offset_modifier",
"ltft_balance_offset",
+34 -1
View File
@@ -191,6 +191,31 @@ def rota_detail(request, rota_id):
except Exception:
configured_constraints = {}
# Compute a small rota-builder summary for display in the template
try:
# Prefer explicit value stored in rota.options['weeks'] when present.
opts = rota.options or {}
if "weeks" in opts:
try:
weeks_to_rota = max(1, int(opts.get("weeks")))
except Exception:
weeks_to_rota = None
else:
delta = rota.end_date - rota.start_date
weeks_to_rota = max(1, int(delta.days // 7))
except Exception:
weeks_to_rota = None
try:
shift_count = len(rota.shifts or [])
except Exception:
shift_count = None
try:
worker_count = rota.workers.count()
except Exception:
worker_count = None
# legacy RotaOptionsForm not used here; we provide a typed RotaScheduleForm below
# Provide a typed RotaScheduleForm instance so the template can render
@@ -205,7 +230,15 @@ def rota_detail(request, rota_id):
return render(
request,
"rota/rota_detail.html",
{"rota": rota, "options_form": typed_form, "available_constraints": available_constraints_rich, "configured_constraints": configured_constraints},
{
"rota": rota,
"options_form": typed_form,
"available_constraints": available_constraints_rich,
"configured_constraints": configured_constraints,
"weeks_to_rota": weeks_to_rota,
"shift_count": shift_count,
"worker_count": worker_count,
},
)