Add rota-builder summary to detail view with weeks, shifts, and workers count
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user