.
This commit is contained in:
@@ -0,0 +1,36 @@
|
|||||||
|
{% load crispy_forms_tags %}
|
||||||
|
<form method="post" hx-post="{{ form_action }}" hx-target="#constraint-row-{{ initial.key|default:"new" }}" hx-swap="outerHTML">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">Key</label>
|
||||||
|
<div class="control">
|
||||||
|
<input class="input" name="key" type="text" value="{{ initial.key }}" {% if initial.key %}readonly{% endif %} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">Type</label>
|
||||||
|
<div class="control">
|
||||||
|
<div class="select">
|
||||||
|
<select name="type">
|
||||||
|
<option value="string" {% if initial.type == 'string' %}selected{% endif %}>String</option>
|
||||||
|
<option value="int" {% if initial.type == 'int' %}selected{% endif %}>Integer</option>
|
||||||
|
<option value="float" {% if initial.type == 'float' %}selected{% endif %}>Float</option>
|
||||||
|
<option value="bool" {% if initial.type == 'bool' %}selected{% endif %}>Boolean</option>
|
||||||
|
<option value="json" {% if initial.type == 'json' %}selected{% endif %}>JSON (list/object)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">Value</label>
|
||||||
|
<div class="control">
|
||||||
|
<textarea name="value" class="textarea" rows="3">{{ initial.value }}</textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<div class="control">
|
||||||
|
<button class="button is-primary" type="submit">Save</button>
|
||||||
|
<button class="button" type="button" onclick="this.closest('form').outerHTML='';">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<div id="constraint-row-{{ key }}" class="constraint-row">
|
||||||
|
<div class="field is-grouped is-grouped-multiline">
|
||||||
|
<div class="control" style="min-width:25%;"><label class="label"><code>{{ key }}</code></label></div>
|
||||||
|
<div class="control" style="min-width:65%;"><pre style="white-space:pre-wrap; margin:0;">{{ pretty }}</pre></div>
|
||||||
|
<div class="control" style="min-width:10%;">
|
||||||
|
<button class="button is-small is-light" hx-get="{% url 'rota:rota_option_inline' rota.id %}?key={{ key }}" hx-target="#constraint-row-{{ key }}" hx-swap="outerHTML">Edit</button>
|
||||||
|
<form method="post" hx-post="{% url 'rota:rota_option_delete' rota.id %}" hx-target="#constraint-row-{{ key }}" hx-swap="outerHTML" style="display:inline">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="key" value="{{ key }}" />
|
||||||
|
<input type="hidden" name="inline" value="1" />
|
||||||
|
<button class="button is-small is-danger is-light" type="submit">Delete</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -64,6 +64,38 @@
|
|||||||
<button class="button is-link" hx-get="{% url 'rota:rota_options' rota.id %}" hx-target="#modal" hx-swap="innerHTML">Edit constraint options</button>
|
<button class="button is-link" hx-get="{% url 'rota:rota_options' rota.id %}" hx-target="#modal" hx-swap="innerHTML">Edit constraint options</button>
|
||||||
</p>
|
</p>
|
||||||
<hr />
|
<hr />
|
||||||
|
<h3 class="subtitle is-6">Configured constraint values</h3>
|
||||||
|
<div style="max-height:200px; overflow:auto; margin-bottom:0.75em;">
|
||||||
|
{% if configured_constraints %}
|
||||||
|
<table class="table is-fullwidth is-striped is-narrow">
|
||||||
|
<thead>
|
||||||
|
<tr><th>Key</th><th>Value</th><th></th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for key, info in configured_constraints.items %}
|
||||||
|
<tr id="constraint-row-{{ key }}">
|
||||||
|
<td style="vertical-align:middle;"><code>{{ key }}</code></td>
|
||||||
|
<td style="vertical-align:middle;"><pre style="white-space:pre-wrap; margin:0;">{{ info.pretty }}</pre></td>
|
||||||
|
<td style="vertical-align:middle;">
|
||||||
|
<button class="button is-small is-light" hx-get="{% url 'rota:rota_option_inline' rota.id %}?key={{ key }}" hx-target="#constraint-row-{{ key }}" hx-swap="outerHTML">Edit</button>
|
||||||
|
<form method="post" hx-post="{% url 'rota:rota_option_delete' rota.id %}" hx-target="#constraint-row-{{ key }}" hx-swap="outerHTML" style="display:inline">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="key" value="{{ key }}" />
|
||||||
|
<input type="hidden" name="inline" value="1" />
|
||||||
|
<button class="button is-small is-danger is-light" type="submit">Delete</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% else %}
|
||||||
|
<p class="help">No typed constraint options have been configured for this rota.</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
<h3 class="subtitle is-6">Available constraint keys</h3>
|
||||||
<div style="max-height:300px; overflow:auto;">
|
<div style="max-height:300px; overflow:auto;">
|
||||||
<table class="table is-fullwidth is-striped is-narrow">
|
<table class="table is-fullwidth is-striped is-narrow">
|
||||||
<thead>
|
<thead>
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ urlpatterns = [
|
|||||||
path("rota/<int:rota_id>/options/", views.rota_options, name="rota_options"),
|
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/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/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>/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("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"),
|
path("run/<int:run_id>/export/regenerate/", views.rota_run_export_regenerate, name="rota_run_export_regenerate"),
|
||||||
|
|||||||
+113
-1
@@ -80,6 +80,23 @@ def rota_detail(request, rota_id):
|
|||||||
except Exception:
|
except Exception:
|
||||||
available_constraints_rich = {k: {"default": v} for k, v in available_constraints.items()}
|
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
|
# legacy RotaOptionsForm not used here; we provide a typed RotaScheduleForm below
|
||||||
|
|
||||||
# Provide a typed RotaScheduleForm instance so the template can render
|
# Provide a typed RotaScheduleForm instance so the template can render
|
||||||
@@ -94,7 +111,7 @@ def rota_detail(request, rota_id):
|
|||||||
return render(
|
return render(
|
||||||
request,
|
request,
|
||||||
"rota/rota_detail.html",
|
"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)
|
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):
|
def rota_option_edit(request, rota_id):
|
||||||
rota = get_object_or_404(RotaSchedule, pk=rota_id)
|
rota = get_object_or_404(RotaSchedule, pk=rota_id)
|
||||||
is_hx = request.headers.get("HX-Request", "false").lower() == "true"
|
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.options = opts
|
||||||
rota.save(update_fields=["options"])
|
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":
|
if request.headers.get("HX-Request", "false").lower() == "true":
|
||||||
return _oob_update_options_display(request, rota)
|
return _oob_update_options_display(request, rota)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user