diff --git a/djangorota/djangorota/templates/rota/rota_detail.html b/djangorota/djangorota/templates/rota/rota_detail.html index c02092f..77a8cbf 100644 --- a/djangorota/djangorota/templates/rota/rota_detail.html +++ b/djangorota/djangorota/templates/rota/rota_detail.html @@ -11,6 +11,19 @@
{{ rota.description }}
Period: {{ rota.start_date }} → {{ rota.end_date }}
+diff --git a/djangorota/rota/forms.py b/djangorota/rota/forms.py index 1d2b1b7..c283aae 100644 --- a/djangorota/rota/forms.py +++ b/djangorota/rota/forms.py @@ -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 diff --git a/djangorota/rota/models.py b/djangorota/rota/models.py index 932ae65..48ffc2c 100644 --- a/djangorota/rota/models.py +++ b/djangorota/rota/models.py @@ -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", diff --git a/djangorota/rota/views.py b/djangorota/rota/views.py index 512a795..9b7a0f2 100644 --- a/djangorota/rota/views.py +++ b/djangorota/rota/views.py @@ -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, + }, )