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 %}
+
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 @@
+
+
+
+
+
+
+
+
+
+
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 %}
+
+
+ | Key | Value | |
+
+
+ {% for key, info in configured_constraints.items %}
+
+ {{ key }} |
+ {{ info.pretty }} |
+
+
+
+ |
+
+ {% endfor %}
+
+
+ {% 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)