This commit is contained in:
Ross
2025-12-09 21:35:38 +00:00
parent 2de9986149
commit d28b441edd
3 changed files with 149 additions and 64 deletions
+114
View File
@@ -204,6 +204,120 @@ class RotaOptionsForm(forms.Form):
)
class RotaConstraintOptionsForm(forms.Form):
"""Dynamically generated form exposing typed `RotaConstraintOptions` fields.
Fields are created as `opt__{key}` to match existing modal rendering logic
and to allow reuse of the `rota_options` endpoint/modal. Saving via
`save_for_rota(rota)` will update only the constraint keys inside
`RotaSchedule.options`, leaving other keys (e.g. legacy builder args)
untouched.
"""
def __init__(self, *args, initial_options=None, **kwargs):
"""initial_options: dict of existing rota.options to populate current values."""
super().__init__(*args, **kwargs)
initial_options = initial_options or {}
# Try to import the typed model from the rota package. Prefer the
# RotaConstraintOptions class (no heavy instantiation) to avoid any
# side-effects of creating a RotaBuilder instance during form init.
opts_model = None
try:
mod = importlib.import_module("rota_generator.shifts")
RotaConstraintOptions = getattr(mod, "RotaConstraintOptions", None)
if RotaConstraintOptions is not None:
opts_model = RotaConstraintOptions()
else:
# Last-resort: try to instantiate RotaBuilder and read its
# constraint_options_model attribute (may have side-effects).
RotaBuilder = getattr(mod, "RotaBuilder", None)
if RotaBuilder is not None:
try:
rb = RotaBuilder()
opts_model = getattr(rb, "constraint_options_model", None)
except Exception:
opts_model = None
except Exception:
opts_model = None
self._constraint_defaults = {}
self._constraint_meta = {}
if opts_model is not None:
try:
defaults = opts_model.model_dump()
except Exception:
defaults = {}
self._constraint_defaults = defaults
# try to read field descriptions
try:
mf = opts_model.__class__.model_fields
except Exception:
mf = {}
for k, v in defaults.items():
field_name = f"opt__{k}"
help_text = None
if isinstance(mf, dict) and k in mf:
info = mf[k]
try:
if hasattr(info, "description") and info.description:
help_text = info.description
else:
help_text = info.get("description")
except Exception:
help_text = None
initial = initial_options.get(k, v)
# Map Python types to Django form fields
if isinstance(v, bool):
self.fields[field_name] = forms.BooleanField(required=False, initial=bool(initial), label=k.replace("_", " "), help_text=help_text)
elif isinstance(v, int):
# Allow None for Optional[int]
self.fields[field_name] = forms.IntegerField(required=False, initial=initial, label=k.replace("_", " "), help_text=help_text)
elif isinstance(v, float):
self.fields[field_name] = forms.FloatField(required=False, initial=initial, label=k.replace("_", " "), help_text=help_text)
elif isinstance(v, (list, dict)):
# JSON textarea for structured values
try:
init_val = json.dumps(initial) if initial is not None else json.dumps(v)
except Exception:
init_val = ""
self.fields[field_name] = forms.CharField(required=False, initial=init_val, widget=forms.Textarea, label=k.replace("_", " "), help_text=(help_text or "Enter JSON"))
else:
self.fields[field_name] = forms.CharField(required=False, initial=initial if initial is not None else v, label=k.replace("_", " "), help_text=help_text)
def save_for_rota(self, rota):
"""Persist cleaned constraint fields into `rota.options`.
Only updates keys that are present in the typed constraint defaults.
For structured fields (JSON), attempt to parse into Python objects.
"""
opts = rota.options or {}
for key in self._constraint_defaults.keys():
field_name = f"opt__{key}"
if field_name in self.cleaned_data:
val = self.cleaned_data[field_name]
default = self._constraint_defaults.get(key)
if isinstance(default, (list, dict)):
try:
parsed = json.loads(val) if val is not None and val != "" else []
except Exception:
parsed = val
opts[key] = parsed
else:
# For optional int/float fields allow empty -> None
if val == "" or val is None:
opts[key] = None
else:
opts[key] = val
rota.options = opts
rota.save(update_fields=["options"])
class ShiftForm(forms.Form):
name = forms.CharField(max_length=200)
sites = forms.CharField(