From 4011fa362701a0d53c098dff66ee34b130c167f9 Mon Sep 17 00:00:00 2001 From: Ross Date: Tue, 9 Dec 2025 21:48:32 +0000 Subject: [PATCH] . --- .../rota/partials/rota_option_inline.html | 36 ++++++ .../rota/partials/rota_option_row.html | 15 +++ .../templates/rota/rota_detail.html | 32 +++++ djangorota/rota/urls.py | 1 + djangorota/rota/views.py | 114 +++++++++++++++++- 5 files changed, 197 insertions(+), 1 deletion(-) create mode 100644 djangorota/djangorota/templates/rota/partials/rota_option_inline.html create mode 100644 djangorota/djangorota/templates/rota/partials/rota_option_row.html diff --git a/djangorota/djangorota/templates/rota/partials/rota_option_inline.html b/djangorota/djangorota/templates/rota/partials/rota_option_inline.html new file mode 100644 index 0000000..2afde87 --- /dev/null +++ b/djangorota/djangorota/templates/rota/partials/rota_option_inline.html @@ -0,0 +1,36 @@ +{% load crispy_forms_tags %} +
+ {% csrf_token %} +
+ +
+ +
+
+
+ +
+
+ +
+
+
+
+ +
+ +
+
+
+
+ + +
+
+
diff --git a/djangorota/djangorota/templates/rota/partials/rota_option_row.html b/djangorota/djangorota/templates/rota/partials/rota_option_row.html new file mode 100644 index 0000000..34cb1a4 --- /dev/null +++ b/djangorota/djangorota/templates/rota/partials/rota_option_row.html @@ -0,0 +1,15 @@ +
+
+
+
{{ pretty }}
+
+ +
+ {% csrf_token %} + + + +
+
+
+
diff --git a/djangorota/djangorota/templates/rota/rota_detail.html b/djangorota/djangorota/templates/rota/rota_detail.html index b57f36c..4a9ee4e 100644 --- a/djangorota/djangorota/templates/rota/rota_detail.html +++ b/djangorota/djangorota/templates/rota/rota_detail.html @@ -64,6 +64,38 @@


+

Configured constraint values

+
+ {% if configured_constraints %} + + + + + + {% for key, info in configured_constraints.items %} + + + + + + {% endfor %} + +
KeyValue
{{ key }}
{{ info.pretty }}
+ +
+ {% csrf_token %} + + + +
+
+ {% else %} +

No typed constraint options have been configured for this rota.

+ {% endif %} +
+ +
+

Available constraint keys

diff --git a/djangorota/rota/urls.py b/djangorota/rota/urls.py index 971035e..45507d7 100644 --- a/djangorota/rota/urls.py +++ b/djangorota/rota/urls.py @@ -24,6 +24,7 @@ urlpatterns = [ path("rota//options/", views.rota_options, name="rota_options"), path("rota//option/add/", views.rota_option_add, name="rota_option_add"), path("rota//option/edit/", views.rota_option_edit, name="rota_option_edit"), + path("rota//option/inline/", views.rota_option_inline, name="rota_option_inline"), path("rota//option/delete/", views.rota_option_delete, name="rota_option_delete"), path("rota//runs/clear/", views.rota_runs_clear, name="rota_runs_clear"), path("run//export/regenerate/", views.rota_run_export_regenerate, name="rota_run_export_regenerate"), diff --git a/djangorota/rota/views.py b/djangorota/rota/views.py index fe44f3e..ef9bbd2 100644 --- a/djangorota/rota/views.py +++ b/djangorota/rota/views.py @@ -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)