ixg_platform/templates/ops/specification_form.html
ixgadmin d74f0814da Finish the wiki root move, zone map view and spec approval
- Follow home/user-guides/ through one GUIDE_PATH_PREFIX constant.
- Add a read-only Origin Zone detail view with the map as the main panel.
- Name due diligence explicitly and add its section headings.
- Add admin-only, four-eyes specification approval (portal and API).
- Empty the New-zone geometry placeholder instead of showing null.
2026-10-27 17:41:08 +00:00

232 lines
9.7 KiB
HTML

{% extends 'ops/base_ops.html' %}
{% block title %}{{ page_title }} - IXG Operations{% endblock %}
{% block content %}
<div class="page-header d-flex justify-content-between align-items-center">
<h2>{{ page_title }}</h2>
<a href="{{ cancel_url }}" class="btn btn-sm btn-outline-secondary"><i class="bi bi-arrow-left"></i> Back</a>
</div>
{% if messages %}
{% for m in messages %}<div class="alert alert-{{ m.tags }} py-2">{{ m }}</div>{% endfor %}
{% endif %}
{% if editing_approved %}
<div class="alert alert-warning">
<strong>This specification is not a draft.</strong> Its parameters are the limits QC results
are judged against, so editing them changes those limits.
</div>
{% endif %}
<form method="post" novalidate>
{% csrf_token %}
<div class="row g-3">
<div class="col-md-7">
<div class="stat-card mb-3">
<h6 class="mb-3">Specification</h6>
{% if form.non_field_errors %}
{% for e in form.non_field_errors %}<div class="alert alert-danger py-2">{{ e }}</div>{% endfor %}
{% endif %}
{% for field in form %}
<div class="mb-3">
<label class="form-label" for="{{ field.id_for_label }}">
{{ field.label }}{% if field.field.required %} <span class="text-danger">*</span>{% endif %}
</label>
{{ field }}
{% if field.help_text %}<div class="form-text">{{ field.help_text }}</div>{% endif %}
{% for e in field.errors %}<div class="text-danger small">{{ e }}</div>{% endfor %}
</div>
{% endfor %}
</div>
<div class="stat-card">
<div class="d-flex justify-content-between align-items-center mb-3">
<h6 class="mb-0">Parameters</h6>
<button type="button" class="btn btn-sm btn-outline-primary" id="add-param"><i class="bi bi-plus-lg"></i> Add parameter</button>
</div>
{{ formset.management_form }}
{% if formset.non_form_errors %}
{% for e in formset.non_form_errors %}<div class="alert alert-danger py-2">{{ e }}</div>{% endfor %}
{% endif %}
<div id="param-rows">
{% for pf in formset %}
<div class="param-row border rounded p-3 mb-3">
{{ pf.id }}
<div class="row g-2">
<div class="col-md-4">
<label class="form-label small" for="{{ pf.parameter.id_for_label }}">Parameter</label>
{{ pf.parameter }}
{% for e in pf.parameter.errors %}<div class="text-danger small">{{ e }}</div>{% endfor %}
</div>
<div class="col-md-3">
<label class="form-label small" for="{{ pf.unit.id_for_label }}">Unit</label>
{{ pf.unit }}
</div>
<div class="col-md-2">
<label class="form-label small" for="{{ pf.min_value.id_for_label }}">Minimum</label>
{{ pf.min_value }}
{% for e in pf.min_value.errors %}<div class="text-danger small">{{ e }}</div>{% endfor %}
</div>
<div class="col-md-2">
<label class="form-label small" for="{{ pf.max_value.id_for_label }}">Maximum</label>
{{ pf.max_value }}
{% for e in pf.max_value.errors %}<div class="text-danger small">{{ e }}</div>{% endfor %}
</div>
<div class="col-md-1">
<label class="form-label small" for="{{ pf.sort_order.id_for_label }}">Order</label>
{{ pf.sort_order }}
</div>
<div class="col-md-5">
<label class="form-label small" for="{{ pf.allowed_values.id_for_label }}">Allowed values (visual grade)</label>
{{ pf.allowed_values }}
{% for e in pf.allowed_values.errors %}<div class="text-danger small">{{ e }}</div>{% endfor %}
</div>
<div class="col-md-3 d-flex align-items-center pt-3">
<div class="form-check">
{{ pf.required }}
<label class="form-check-label small" for="{{ pf.required.id_for_label }}">Required</label>
</div>
</div>
<div class="col-md-2 d-flex align-items-center pt-3">
<div class="form-check">
{{ pf.DELETE }}
<label class="form-check-label small text-danger" for="{{ pf.DELETE.id_for_label }}">Remove</label>
</div>
</div>
</div>
{% for e in pf.non_field_errors %}<div class="text-danger small">{{ e }}</div>{% endfor %}
</div>
{% endfor %}
</div>
</div>
</div>
<div class="col-md-5">
<div class="stat-card mb-3">
<h6 class="mb-3"><i class="bi bi-info-circle"></i> How a specification is used</h6>
<p class="small mb-2">
Each parameter is a limit. A numeric parameter is checked between its minimum and
maximum; a visual parameter is checked against its allowed grades. A QC result is
judged against the specification that is both <strong>approved</strong> and
<strong>current</strong>.
</p>
<p class="small mb-0 text-secondary">
A specification needs at least one parameter: with none, every QC result would pass.
</p>
</div>
<div class="stat-card">
<h6 class="mb-3"><i class="bi bi-shield-lock"></i> Approval (four-eyes)</h6>
{% if object and object.pk %}
{% if object.status == 'approved' %}
<p class="small mb-0">
Approved{% if object.approved_by %} by {{ object.approved_by.full_name|default:object.approved_by.email }}{% endif %}{% if object.approved_at %} on {{ object.approved_at|date:'d/m/Y H:i' }}{% endif %}.
{% if object.is_current %}It is the current specification for this product.{% endif %}
</p>
{% elif object.status == 'superseded' %}
<p class="small mb-0">This version is superseded. It can no longer be approved.</p>
{% elif show_approval %}
{% if approval_error %}
<p class="small mb-2 text-danger">{{ approval_error }}</p>
{% else %}
<p class="small mb-2">
Approving makes these parameters the limits QC is judged against, and
supersedes the product's previously current version.
</p>
<form method="post" action="{% url 'ops:specification_approve' object.pk %}"
onsubmit="return confirm('Approve this specification and make it current? This changes the limits QC is judged against.');">
{% csrf_token %}
<button type="submit" class="btn btn-sm btn-success w-100"><i class="bi bi-check-circle"></i> Approve and make current</button>
</form>
{% endif %}
{% else %}
<p class="small mb-0">
Approval is an administrator action. Save this specification as a draft; an
administrator can then approve it. The creator may not approve their own
specification.
</p>
{% endif %}
{% else %}
<p class="small mb-0">
A new specification is saved as a draft. Approval is a separate administrator
action, and the creator may not approve their own specification, so a second
person makes the limits live.
</p>
{% endif %}
</div>
</div>
</div>
<div class="mt-3">
<button type="submit" class="btn btn-primary">{{ submit_label }}</button>
<a href="{{ cancel_url }}" class="btn btn-outline-secondary">Cancel</a>
</div>
</form>
<template id="empty-param-row">
<div class="param-row border rounded p-3 mb-3">
{{ formset.empty_form.id }}
<div class="row g-2">
<div class="col-md-4">
<label class="form-label small">Parameter</label>
{{ formset.empty_form.parameter }}
</div>
<div class="col-md-3">
<label class="form-label small">Unit</label>
{{ formset.empty_form.unit }}
</div>
<div class="col-md-2">
<label class="form-label small">Minimum</label>
{{ formset.empty_form.min_value }}
</div>
<div class="col-md-2">
<label class="form-label small">Maximum</label>
{{ formset.empty_form.max_value }}
</div>
<div class="col-md-1">
<label class="form-label small">Order</label>
{{ formset.empty_form.sort_order }}
</div>
<div class="col-md-5">
<label class="form-label small">Allowed values (visual grade)</label>
{{ formset.empty_form.allowed_values }}
</div>
<div class="col-md-3 d-flex align-items-center pt-3">
<div class="form-check">
{{ formset.empty_form.required }}
<label class="form-check-label small">Required</label>
</div>
</div>
<div class="col-md-2 d-flex align-items-center pt-3">
<div class="form-check">
{{ formset.empty_form.DELETE }}
<label class="form-check-label small text-danger">Remove</label>
</div>
</div>
</div>
</div>
</template>
{% endblock %}
{% block extra_js %}
<script>
(function () {
var total = document.getElementById('id_params-TOTAL_FORMS');
var add = document.getElementById('add-param');
var tpl = document.getElementById('empty-param-row');
var rows = document.getElementById('param-rows');
if (!total || !add || !tpl || !rows) { return; }
add.addEventListener('click', function () {
var index = parseInt(total.value, 10);
var html = tpl.innerHTML.replace(/__prefix__/g, index);
var holder = document.createElement('div');
holder.innerHTML = html.trim();
rows.appendChild(holder.firstElementChild);
total.value = index + 1;
});
})();
</script>
{% endblock %}