This commit is contained in:
Ross
2025-12-09 21:48:32 +00:00
parent d28b441edd
commit 4011fa3627
5 changed files with 197 additions and 1 deletions
+1
View File
@@ -24,6 +24,7 @@ urlpatterns = [
path("rota/<int:rota_id>/options/", views.rota_options, name="rota_options"),
path("rota/<int:rota_id>/option/add/", views.rota_option_add, name="rota_option_add"),
path("rota/<int:rota_id>/option/edit/", views.rota_option_edit, name="rota_option_edit"),
path("rota/<int:rota_id>/option/inline/", views.rota_option_inline, name="rota_option_inline"),
path("rota/<int:rota_id>/option/delete/", views.rota_option_delete, name="rota_option_delete"),
path("rota/<int:rota_id>/runs/clear/", views.rota_runs_clear, name="rota_runs_clear"),
path("run/<int:run_id>/export/regenerate/", views.rota_run_export_regenerate, name="rota_run_export_regenerate"),
+113 -1
View File
@@ -80,6 +80,23 @@ def rota_detail(request, rota_id):
except Exception:
available_constraints_rich = {k: {"default": v} for k, v in available_constraints.items()}
# Build a mapping of configured constraint keys (from rota.options).
# Include all keys present in rota.options so they can be displayed and
# edited individually on the main page, even if the typed defaults are
# unavailable. Mark whether each key appears in the typed defaults.
configured_constraints = {}
try:
opts = rota.options or {}
for k, v in opts.items():
# pretty-print complex values where appropriate
try:
pretty = json.dumps(v, indent=2, default=str)
except Exception:
pretty = str(v)
configured_constraints[k] = {"value": v, "pretty": pretty, "typed": (k in available_constraints_rich)}
except Exception:
configured_constraints = {}
# legacy RotaOptionsForm not used here; we provide a typed RotaScheduleForm below
# Provide a typed RotaScheduleForm instance so the template can render
@@ -94,7 +111,7 @@ def rota_detail(request, rota_id):
return render(
request,
"rota/rota_detail.html",
{"rota": rota, "options_form": typed_form, "available_constraints": available_constraints_rich},
{"rota": rota, "options_form": typed_form, "available_constraints": available_constraints_rich, "configured_constraints": configured_constraints},
)
@@ -475,6 +492,95 @@ def rota_option_add(request, rota_id):
return HttpResponse(modal_html)
def rota_option_inline(request, rota_id):
"""Return an inline editor for a single option (or save it).
GET: return a small form fragment suitable for in-place replacement of a
constraint row. POST: save the key/value and return the rendered row
fragment to replace the editor.
"""
rota = get_object_or_404(RotaSchedule, pk=rota_id)
if request.method == "POST":
key = request.POST.get("key", "").strip()
val = request.POST.get("value", "")
vtype = request.POST.get("type", "string")
if not key:
return HttpResponseBadRequest("Key is required")
try:
if vtype == "int":
parsed = int(val)
elif vtype == "float":
parsed = float(val)
elif vtype == "bool":
parsed = val.lower() in ("1", "true", "yes", "on")
elif vtype == "json":
parsed = json.loads(val) if val.strip() else None
else:
parsed = val
except Exception as exc:
return HttpResponseBadRequest(f"Invalid value: {exc}")
opts = rota.options or {}
opts[key] = parsed
rota.options = opts
rota.save(update_fields=["options"])
# Render and return the updated row fragment
try:
pretty = json.dumps(parsed, indent=2, default=str)
except Exception:
pretty = str(parsed)
row_html = render_to_string(
"rota/partials/rota_option_row.html",
{"rota": rota, "key": key, "value": parsed, "pretty": pretty},
request=request,
)
return HttpResponse(row_html)
# GET: return inline form fragment prefilled from defaults if present
key_q = request.GET.get("key")
initial = {"key": "", "value": "", "type": "string"}
if key_q:
try:
import importlib as _importlib
mod = _importlib.import_module("rota_generator.shifts")
RotaBuilder = getattr(mod, "RotaBuilder")
rb = RotaBuilder()
defaults = rb.constraint_options_model.model_dump()
if key_q in defaults:
d = defaults[key_q]
initial["key"] = key_q
if isinstance(d, bool):
initial["type"] = "bool"
initial["value"] = "1" if d else "0"
elif isinstance(d, int):
initial["type"] = "int"
initial["value"] = str(d)
elif isinstance(d, float):
initial["type"] = "float"
initial["value"] = str(d)
elif isinstance(d, (list, dict)):
initial["type"] = "json"
try:
initial["value"] = json.dumps(d)
except Exception:
initial["value"] = ""
else:
initial["type"] = "string"
initial["value"] = str(d)
except Exception:
pass
inline_html = render_to_string(
"rota/partials/rota_option_inline.html",
{"form_action": reverse("rota:rota_option_inline", args=(rota.id,)), "initial": initial, "rota": rota},
request=request,
)
return HttpResponse(inline_html)
def rota_option_edit(request, rota_id):
rota = get_object_or_404(RotaSchedule, pk=rota_id)
is_hx = request.headers.get("HX-Request", "false").lower() == "true"
@@ -552,6 +658,12 @@ def rota_option_delete(request, rota_id):
rota.options = opts
rota.save(update_fields=["options"])
# If this was an inline request, return an empty fragment so the
# inline editor row can be removed client-side. Otherwise return the
# standard HTMX OOB update to refresh the options display.
if request.POST.get("inline") == "1":
return HttpResponse("")
if request.headers.get("HX-Request", "false").lower() == "true":
return _oob_update_options_display(request, rota)