.
This commit is contained in:
@@ -1,39 +1,43 @@
|
|||||||
{% load crispy_forms_tags %}
|
{% load crispy_forms_tags %}
|
||||||
<form method="post" hx-post="{{ form_action }}" hx-target="#constraint-row-{{ initial.key|default:"new" }}" hx-swap="outerHTML">
|
<tr id="constraint-row-{{ initial.key|default:"new" }}">
|
||||||
{% csrf_token %}
|
<td colspan="3">
|
||||||
{% if error %}
|
<form method="post" hx-post="{{ form_action }}" hx-target="#constraint-row-{{ initial.key|default:"new" }}" hx-swap="outerHTML">
|
||||||
<div class="notification is-danger">{{ error }}</div>
|
{% csrf_token %}
|
||||||
{% endif %}
|
{% if error %}
|
||||||
<div class="field">
|
<div class="notification is-danger">{{ error }}</div>
|
||||||
<label class="label">Key</label>
|
{% endif %}
|
||||||
<div class="control">
|
<div class="field">
|
||||||
<input class="input" name="key" type="text" value="{{ initial.key }}" {% if initial.key %}readonly{% endif %} />
|
<label class="label">Key</label>
|
||||||
</div>
|
<div class="control">
|
||||||
</div>
|
<input class="input" name="key" type="text" value="{{ initial.key }}" {% if initial.key %}readonly{% endif %} />
|
||||||
<div class="field">
|
</div>
|
||||||
<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">
|
||||||
</div>
|
<label class="label">Type</label>
|
||||||
<div class="field">
|
<div class="control">
|
||||||
<label class="label">Value</label>
|
<div class="select">
|
||||||
<div class="control">
|
<select name="type">
|
||||||
<textarea name="value" class="textarea" rows="3">{{ initial.value }}</textarea>
|
<option value="string" {% if initial.type == 'string' %}selected{% endif %}>String</option>
|
||||||
</div>
|
<option value="int" {% if initial.type == 'int' %}selected{% endif %}>Integer</option>
|
||||||
</div>
|
<option value="float" {% if initial.type == 'float' %}selected{% endif %}>Float</option>
|
||||||
<div class="field">
|
<option value="bool" {% if initial.type == 'bool' %}selected{% endif %}>Boolean</option>
|
||||||
<div class="control">
|
<option value="json" {% if initial.type == 'json' %}selected{% endif %}>JSON (list/object)</option>
|
||||||
<button class="button is-primary" type="submit">Save</button>
|
</select>
|
||||||
<button class="button" type="button" onclick="this.closest('form').outerHTML='';">Cancel</button>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
<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('tr').outerHTML='';">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|||||||
@@ -9,6 +9,11 @@ function _close_rota_option_modal(){
|
|||||||
<div class="modal-card">
|
<div class="modal-card">
|
||||||
<header class="modal-card-head">
|
<header class="modal-card-head">
|
||||||
<p class="modal-card-title">{{ initial.key|default:'Add option' }}</p>
|
<p class="modal-card-title">{{ initial.key|default:'Add option' }}</p>
|
||||||
|
{% if description %}
|
||||||
|
<div style="margin-left:1.25em; margin-top:0.25em;">
|
||||||
|
<p class="is-size-7 has-text-grey">{{ description }}</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
<button class="delete" aria-label="close" onclick="_close_rota_option_modal()"></button>
|
<button class="delete" aria-label="close" onclick="_close_rota_option_modal()"></button>
|
||||||
</header>
|
</header>
|
||||||
<section class="modal-card-body">
|
<section class="modal-card-body">
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<div id="constraint-row-{{ key }}" class="constraint-row">
|
<div id="constraint-row-{{ key }}" class="constraint-row">
|
||||||
<div class="field is-grouped is-grouped-multiline">
|
<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:25%;"><label class="label"><code title="{{ description }}">{{ 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:65%;"><pre style="white-space:pre-wrap; margin:0;">{{ pretty }}</pre></div>
|
||||||
<div class="control" style="min-width:10%;">
|
<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>
|
<a href="javascript:void(0)" class="button is-small is-light" role="button" hx-get="{% url 'rota:rota_option_edit' rota.id %}?key={{ key }}" hx-target="#modal" hx-swap="innerHTML">Edit</a>
|
||||||
<form method="post" hx-post="{% url 'rota:rota_option_delete' rota.id %}" hx-target="#constraint-row-{{ key }}" hx-swap="outerHTML" style="display:inline">
|
<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 %}
|
{% csrf_token %}
|
||||||
<input type="hidden" name="key" value="{{ key }}" />
|
<input type="hidden" name="key" value="{{ key }}" />
|
||||||
|
|||||||
@@ -74,10 +74,10 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
{% for key, info in configured_constraints.items %}
|
{% for key, info in configured_constraints.items %}
|
||||||
<tr id="constraint-row-{{ key }}">
|
<tr id="constraint-row-{{ key }}">
|
||||||
<td style="vertical-align:middle;"><code>{{ key }}</code></td>
|
<td style="vertical-align:middle;"><code title="{{ info.description|default:'' }}">{{ 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;"><pre style="white-space:pre-wrap; margin:0;">{{ info.pretty }}</pre></td>
|
||||||
<td style="vertical-align:middle;">
|
<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>
|
<a href="javascript:void(0)" class="button is-small is-light" role="button" hx-get="{% url 'rota:rota_option_edit' rota.id %}?key={{ key }}" hx-target="#modal" hx-swap="innerHTML">Edit</a>
|
||||||
<form method="post" hx-post="{% url 'rota:rota_option_delete' rota.id %}" hx-target="#constraint-row-{{ key }}" hx-swap="outerHTML" style="display:inline">
|
<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 %}
|
{% csrf_token %}
|
||||||
<input type="hidden" name="key" value="{{ key }}" />
|
<input type="hidden" name="key" value="{{ key }}" />
|
||||||
|
|||||||
@@ -14,6 +14,71 @@ from django.shortcuts import HttpResponseRedirect
|
|||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
|
|
||||||
|
def _get_constraint_description(key: str):
|
||||||
|
"""Return the description for a constraint key from the typed model metadata.
|
||||||
|
|
||||||
|
This is defensive because different pydantic versions expose field metadata
|
||||||
|
to the Python API slightly differently.
|
||||||
|
"""
|
||||||
|
if not key:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
import importlib
|
||||||
|
mod = importlib.import_module("rota_generator.shifts")
|
||||||
|
RotaConstraintOptions = getattr(mod, "RotaConstraintOptions", None)
|
||||||
|
meta = {}
|
||||||
|
if RotaConstraintOptions is not None:
|
||||||
|
meta = getattr(RotaConstraintOptions, "model_fields", {}) or {}
|
||||||
|
else:
|
||||||
|
RotaBuilder = getattr(mod, "RotaBuilder", None)
|
||||||
|
if RotaBuilder is not None:
|
||||||
|
try:
|
||||||
|
rb = RotaBuilder()
|
||||||
|
opts_model = getattr(rb, "constraint_options_model", None)
|
||||||
|
meta = getattr(opts_model.__class__, "model_fields", {}) or {}
|
||||||
|
except Exception:
|
||||||
|
meta = {}
|
||||||
|
# meta should be a dict-like mapping of field names -> field info
|
||||||
|
if not isinstance(meta, dict):
|
||||||
|
try:
|
||||||
|
meta = dict(meta)
|
||||||
|
except Exception:
|
||||||
|
meta = {}
|
||||||
|
if key not in meta:
|
||||||
|
return None
|
||||||
|
info = meta[key]
|
||||||
|
# Try several access patterns to extract a description
|
||||||
|
desc = None
|
||||||
|
try:
|
||||||
|
if hasattr(info, "description") and info.description:
|
||||||
|
desc = info.description
|
||||||
|
except Exception:
|
||||||
|
desc = None
|
||||||
|
if not desc:
|
||||||
|
try:
|
||||||
|
# dict-like
|
||||||
|
desc = info.get("description")
|
||||||
|
except Exception:
|
||||||
|
desc = None
|
||||||
|
if not desc:
|
||||||
|
try:
|
||||||
|
fi = getattr(info, "field_info", None)
|
||||||
|
if fi is not None and getattr(fi, "description", None):
|
||||||
|
desc = fi.description
|
||||||
|
except Exception:
|
||||||
|
desc = None
|
||||||
|
if not desc:
|
||||||
|
try:
|
||||||
|
extra = getattr(info, "extra", None)
|
||||||
|
if isinstance(extra, dict) and extra.get("description"):
|
||||||
|
desc = extra.get("description")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return desc
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def index(request):
|
def index(request):
|
||||||
rotas = RotaSchedule.objects.all().order_by("-created_at")
|
rotas = RotaSchedule.objects.all().order_by("-created_at")
|
||||||
return render(request, "rota/index.html", {"rotas": rotas})
|
return render(request, "rota/index.html", {"rotas": rotas})
|
||||||
@@ -93,7 +158,14 @@ def rota_detail(request, rota_id):
|
|||||||
pretty = json.dumps(v, indent=2, default=str)
|
pretty = json.dumps(v, indent=2, default=str)
|
||||||
except Exception:
|
except Exception:
|
||||||
pretty = str(v)
|
pretty = str(v)
|
||||||
configured_constraints[k] = {"value": v, "pretty": pretty, "typed": (k in available_constraints_rich)}
|
# Include description from the typed defaults when available so the
|
||||||
|
# UI can show tooltips and the modal can display helpful text.
|
||||||
|
desc = None
|
||||||
|
try:
|
||||||
|
desc = available_constraints_rich.get(k, {}).get("description")
|
||||||
|
except Exception:
|
||||||
|
desc = None
|
||||||
|
configured_constraints[k] = {"value": v, "pretty": pretty, "typed": (k in available_constraints_rich), "description": desc}
|
||||||
except Exception:
|
except Exception:
|
||||||
configured_constraints = {}
|
configured_constraints = {}
|
||||||
|
|
||||||
@@ -484,9 +556,12 @@ def rota_option_add(request, rota_id):
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# Compute description for the suggested key (if any) and pass into modal
|
||||||
|
desc = _get_constraint_description(key_q) if key_q else None
|
||||||
|
|
||||||
modal_html = render_to_string(
|
modal_html = render_to_string(
|
||||||
"rota/partials/rota_option_modal_single.html",
|
"rota/partials/rota_option_modal_single.html",
|
||||||
{"form_action": reverse("rota:rota_option_add", args=(rota.id,)), "initial": initial, "rota": rota},
|
{"form_action": reverse("rota:rota_option_add", args=(rota.id,)), "initial": initial, "rota": rota, "description": desc},
|
||||||
request=request,
|
request=request,
|
||||||
)
|
)
|
||||||
return HttpResponse(modal_html)
|
return HttpResponse(modal_html)
|
||||||
@@ -551,9 +626,11 @@ def rota_option_inline(request, rota_id):
|
|||||||
pretty = json.dumps(parsed, indent=2, default=str)
|
pretty = json.dumps(parsed, indent=2, default=str)
|
||||||
except Exception:
|
except Exception:
|
||||||
pretty = str(parsed)
|
pretty = str(parsed)
|
||||||
|
# Include description for the saved key so the updated row has a tooltip
|
||||||
|
desc = _get_constraint_description(key)
|
||||||
row_html = render_to_string(
|
row_html = render_to_string(
|
||||||
"rota/partials/rota_option_row.html",
|
"rota/partials/rota_option_row.html",
|
||||||
{"rota": rota, "key": key, "value": parsed, "pretty": pretty},
|
{"rota": rota, "key": key, "value": parsed, "pretty": pretty, "description": desc},
|
||||||
request=request,
|
request=request,
|
||||||
)
|
)
|
||||||
return HttpResponse(row_html)
|
return HttpResponse(row_html)
|
||||||
@@ -658,9 +735,11 @@ def rota_option_edit(request, rota_id):
|
|||||||
vtype = "string"
|
vtype = "string"
|
||||||
value = str(cur_val)
|
value = str(cur_val)
|
||||||
|
|
||||||
|
# Include description for the key in the edit modal
|
||||||
|
desc = _get_constraint_description(key)
|
||||||
modal_html = render_to_string(
|
modal_html = render_to_string(
|
||||||
"rota/partials/rota_option_modal_single.html",
|
"rota/partials/rota_option_modal_single.html",
|
||||||
{"form_action": reverse("rota:rota_option_edit", args=(rota.id,)), "initial": {"key": key, "value": value, "type": vtype}, "rota": rota},
|
{"form_action": reverse("rota:rota_option_edit", args=(rota.id,)), "initial": {"key": key, "value": value, "type": vtype}, "rota": rota, "description": desc},
|
||||||
request=request,
|
request=request,
|
||||||
)
|
)
|
||||||
return HttpResponse(modal_html)
|
return HttpResponse(modal_html)
|
||||||
|
|||||||
Reference in New Issue
Block a user