WIP: Service Plans #308
4 changed files with 385 additions and 135 deletions
|
|
@ -30,14 +30,42 @@
|
|||
{% endpartialdef %}
|
||||
{% block content %}
|
||||
<section class="section">
|
||||
<div class="card">
|
||||
{% if not form and not custom_form %}
|
||||
<div class="alert alert-warning" role="alert">
|
||||
{% translate "Cannot update this service instance because its details could not be retrieved from the underlying system. It might have been deleted externally." %}
|
||||
<form class="form form-vertical crd-form" method="post" novalidate>
|
||||
{% csrf_token %}
|
||||
{% if plan_form.errors or form.errors or custom_form.errors %}
|
||||
<div class="row mt-3">
|
||||
<div class="col-12">
|
||||
{% include "frontend/forms/errors.html" with form=plan_form %}
|
||||
{% if form %}
|
||||
{% include "frontend/forms/errors.html" with form=form %}
|
||||
{% endif %}
|
||||
{% if custom_form %}
|
||||
{% include "frontend/forms/errors.html" with form=custom_form %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div id="service-form">{% partial service-form %}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<!-- Compute Plan Selection -->
|
||||
{% if plan_form %}
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">{% translate "Compute Plan" %}</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% include "includes/plan_selection.html" with plan_form=plan_form storage_plan=storage_plan %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<!-- Service Form -->
|
||||
<div class="card">
|
||||
{% if not form and not custom_form %}
|
||||
<div class="alert alert-warning" role="alert">
|
||||
{% translate "Cannot update this service instance because its details could not be retrieved from the underlying system. It might have been deleted externally." %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div id="service-form">{% partial service-form %}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
{% endblock content %}
|
||||
|
|
|
|||
|
|
@ -124,12 +124,61 @@
|
|||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<!-- Service Form (unchanged) -->
|
||||
<div class="row mt-3">
|
||||
<div class="col-12">
|
||||
<div id="service-form">{% partial service-form %}</div>
|
||||
<form class="form form-vertical crd-form" method="post" novalidate>
|
||||
{% csrf_token %}
|
||||
{% if plan_form.errors or service_form.errors or custom_service_form.errors %}
|
||||
<div class="row mt-3">
|
||||
<div class="col-12">
|
||||
{% include "frontend/forms/errors.html" with form=plan_form %}
|
||||
{% if service_form %}
|
||||
{% include "frontend/forms/errors.html" with form=service_form %}
|
||||
{% endif %}
|
||||
{% if custom_service_form %}
|
||||
{% include "frontend/forms/errors.html" with form=custom_service_form %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<!-- Compute Plan Selection -->
|
||||
{% if context_object %}
|
||||
{% if not has_available_plans %}
|
||||
<div class="row mt-3">
|
||||
<div class="col-12">
|
||||
<div class="alert alert-warning d-flex align-items-center" role="alert">
|
||||
<i class="bi bi-exclamation-triangle-fill me-2"></i>
|
||||
<div>
|
||||
<strong>{% translate "No Compute Plans Available" %}</strong>
|
||||
<p class="mb-0">
|
||||
{% translate "Service instances cannot be created for this offering because no billing plans are configured. Please contact support." %}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="row mt-3">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">{% translate "Select Compute Plan" %}</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% include "includes/plan_selection.html" with plan_form=plan_form storage_plan=storage_plan %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<!-- Service Form -->
|
||||
<div class="row mt-3">
|
||||
<div class="col-12">
|
||||
<fieldset {% if context_object and not has_available_plans %}disabled{% endif %}>
|
||||
<div id="service-form">{% partial service-form %}</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
{% endblock content %}
|
||||
{% block extra_js %}
|
||||
|
|
|
|||
178
src/servala/frontend/templates/includes/plan_selection.html
Normal file
178
src/servala/frontend/templates/includes/plan_selection.html
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
{% load i18n %}
|
||||
<style>
|
||||
.plan-selection .plan-card {
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.plan-selection .plan-card .card {
|
||||
cursor: pointer;
|
||||
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.plan-selection .plan-card .card-body {
|
||||
padding: 0.75rem 1rem;
|
||||
}
|
||||
|
||||
.plan-selection .plan-card input[type="radio"]:checked+label .card {
|
||||
border-color: #a1afdf;
|
||||
box-shadow: 0 0 0 0.25rem rgba(67, 94, 190, 0.25);
|
||||
}
|
||||
|
||||
.plan-selection .plan-card .card:hover {
|
||||
border-color: #a1afdf;
|
||||
}
|
||||
|
||||
.plan-selection .form-check-input {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.plan-selection h6 {
|
||||
margin-bottom: 0.25rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.plan-selection .badge {
|
||||
font-size: 0.75rem;
|
||||
padding: 0.25em 0.5em;
|
||||
}
|
||||
|
||||
.plan-selection .price-display {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.plan-selection .storage-info {
|
||||
margin-top: 0.75rem;
|
||||
padding: 0.75rem 1rem;
|
||||
background-color: #f8f9fa;
|
||||
border-left: 3px solid #6c757d;
|
||||
}
|
||||
</style>
|
||||
<div class="plan-selection">
|
||||
{% if plan_form %}
|
||||
{% for assignment in plan_form.fields.compute_plan_assignment.queryset %}
|
||||
<div class="form-check plan-card">
|
||||
<input class="form-check-input"
|
||||
type="radio"
|
||||
name="{{ plan_form.compute_plan_assignment.html_name }}"
|
||||
id="{{ plan_form.compute_plan_assignment.auto_id }}_{{ forloop.counter0 }}"
|
||||
value="{{ assignment.pk }}"
|
||||
{% if plan_form.compute_plan_assignment.value == assignment.pk|stringformat:"s" or plan_form.fields.compute_plan_assignment.initial == assignment or not plan_form.is_bound and forloop.first %}checked{% endif %}
|
||||
data-memory-limits="{{ assignment.compute_plan.memory_limits }}"
|
||||
data-memory-requests="{{ assignment.compute_plan.memory_requests }}"
|
||||
data-cpu-limits="{{ assignment.compute_plan.cpu_limits }}"
|
||||
data-cpu-requests="{{ assignment.compute_plan.cpu_requests }}"
|
||||
required>
|
||||
<label class="form-check-label w-100"
|
||||
for="{{ plan_form.compute_plan_assignment.auto_id }}_{{ forloop.counter0 }}">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<div>
|
||||
<h6>{{ assignment.compute_plan.name }}</h6>
|
||||
<span class="badge bg-{% if assignment.sla == 'guaranteed' %}success{% else %}secondary{% endif %}">
|
||||
{{ assignment.get_sla_display }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-end">
|
||||
<div class="price-display">CHF {{ assignment.price }}</div>
|
||||
<div class="text-muted small">{% trans "per" %} {{ assignment.get_unit_display }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2 text-muted small">
|
||||
<i class="bi bi-cpu"></i> {{ assignment.compute_plan.cpu_limits }} {% trans "vCPU" %}
|
||||
<span class="mx-2">•</span>
|
||||
<i class="bi bi-memory"></i> {{ assignment.compute_plan.memory_limits }} {% trans "RAM" %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
<div class="storage-info">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<div>
|
||||
<strong class="d-block mb-1">{% trans "Storage" %}</strong>
|
||||
<span class="text-muted small">{% trans "Billed separately based on disk usage" %}</span>
|
||||
</div>
|
||||
{% if storage_plan %}
|
||||
<div class="text-end">
|
||||
<div class="fw-semibold">CHF {{ storage_plan.price_per_gib }}</div>
|
||||
<div class="text-muted small">{% trans "per GiB" %}</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-end text-muted small">{% trans "Included" %}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if storage_plan %}<div class="mt-2 text-muted small" id="storage-cost-display"></div>{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-warning">{% trans "No compute plans available for this service offering." %}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<script>
|
||||
// Update readonly CPU/memory fields when plan selection changes
|
||||
document.querySelectorAll('input[name="{{ plan_form.compute_plan_assignment.html_name }}"]').forEach(radio => {
|
||||
radio.addEventListener('change', function() {
|
||||
if (this.checked) {
|
||||
// Update CPU/memory fields in the form
|
||||
const cpuLimit = document.querySelector('input[name="expert-spec.parameters.size.cpu"]');
|
||||
const memoryLimit = document.querySelector('input[name="expert-spec.parameters.size.memory"]');
|
||||
const cpuRequest = document.querySelector('input[name="expert-spec.parameters.size.requests.cpu"]');
|
||||
const memoryRequest = document.querySelector('input[name="expert-spec.parameters.size.requests.memory"]');
|
||||
|
||||
if (cpuLimit) cpuLimit.value = this.dataset.cpuLimits;
|
||||
if (memoryLimit) memoryLimit.value = this.dataset.memoryLimits;
|
||||
if (cpuRequest) cpuRequest.value = this.dataset.cpuRequests;
|
||||
if (memoryRequest) memoryRequest.value = this.dataset.memoryRequests;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Trigger initial update
|
||||
const checkedRadio = document.querySelector('input[name="{{ plan_form.compute_plan_assignment.html_name }}"]:checked');
|
||||
if (checkedRadio) {
|
||||
checkedRadio.dispatchEvent(new Event('change'));
|
||||
}
|
||||
|
||||
// Setup storage cost calculator
|
||||
function setupStorageCostCalculator() {
|
||||
const diskInput = document.getElementById('id_custom-spec.parameters.size.disk');
|
||||
if (diskInput && !diskInput.dataset.storageListenerAttached) {
|
||||
diskInput.dataset.storageListenerAttached = 'true';
|
||||
diskInput.addEventListener('input', function() {
|
||||
const sizeGiB = parseFloat(this.value) || 0;
|
||||
const pricePerGiB = {
|
||||
{
|
||||
storage_plan.price_per_gib |
|
||||
default: 0
|
||||
}
|
||||
};
|
||||
const totalCost = (sizeGiB * pricePerGiB).toFixed(2);
|
||||
const display = document.getElementById('storage-cost-display');
|
||||
if (display && sizeGiB > 0) {
|
||||
display.innerHTML = '<i class="bi bi-calculator"></i> ' + sizeGiB + ' GiB × CHF ' + pricePerGiB + ' = <strong>CHF ' + totalCost + '</strong> {% trans "per hour" %}';
|
||||
} else if (display) {
|
||||
display.textContent = '';
|
||||
}
|
||||
});
|
||||
|
||||
// Trigger initial calculation if disk field has a value
|
||||
if (diskInput.value) {
|
||||
diskInput.dispatchEvent(new Event('input'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try to setup immediately (in case form is already loaded)
|
||||
setupStorageCostCalculator();
|
||||
|
||||
// Also setup after HTMX swaps the form in
|
||||
document.body.addEventListener('htmx:afterSwap', function(event) {
|
||||
if (event.detail.target.id === 'service-form' || event.detail.target.id === 'control-plane-info') {
|
||||
setupStorageCostCalculator();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
|
@ -1,26 +1,64 @@
|
|||
{% load i18n %}
|
||||
{% load get_field %}
|
||||
{% load static %}
|
||||
<form class="form form-vertical crd-form"
|
||||
method="post"
|
||||
{% if form_action %}action="{{ form_action }}"{% endif %}>
|
||||
{% csrf_token %}
|
||||
{% include "frontend/forms/errors.html" %}
|
||||
{% if form and expert_form and not hide_expert_mode %}
|
||||
<div class="mb-3 text-end">
|
||||
<a href="#"
|
||||
class="text-muted small"
|
||||
id="expert-mode-toggle"
|
||||
style="text-decoration: none">{% translate "Show Expert Mode" %}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div id="custom-form-container"
|
||||
class="{% if form %}custom-crd-form{% else %}expert-crd-form{% endif %}">
|
||||
{% if form and form.context %}{{ form.context }}{% endif %}
|
||||
{% if form and form.get_fieldsets|length == 1 %}
|
||||
{# Single fieldset - render without tabs #}
|
||||
{% include "frontend/forms/errors.html" %}
|
||||
{% if form and expert_form and not hide_expert_mode %}
|
||||
<div class="mb-3 text-end">
|
||||
<a href="#"
|
||||
class="text-muted small"
|
||||
id="expert-mode-toggle"
|
||||
style="text-decoration: none">{% translate "Show Expert Mode" %}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div id="custom-form-container"
|
||||
class="{% if form %}custom-crd-form{% else %}expert-crd-form{% endif %}">
|
||||
{% if form and form.context %}{{ form.context }}{% endif %}
|
||||
{% if form and form.get_fieldsets|length == 1 %}
|
||||
{# Single fieldset - render without tabs #}
|
||||
{% for fieldset in form.get_fieldsets %}
|
||||
<div class="my-2">
|
||||
{% for field in fieldset.fields %}
|
||||
{% with field=form|get_field:field %}{{ field.as_field_group }}{% endwith %}
|
||||
{% endfor %}
|
||||
{% for subfieldset in fieldset.fieldsets %}
|
||||
{% if subfieldset.fields %}
|
||||
<div>
|
||||
<h4 class="mt-3">{{ subfieldset.title }}</h4>
|
||||
{% for field in subfieldset.fields %}
|
||||
{% with field=form|get_field:field %}{{ field.as_field_group }}{% endwith %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% elif form %}
|
||||
{# Multiple fieldsets or auto-generated form - render with tabs #}
|
||||
<ul class="nav nav-tabs" id="myTab" role="tablist">
|
||||
{% for fieldset in form.get_fieldsets %}
|
||||
<div class="my-2">
|
||||
{% if not fieldset.hidden %}
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link {% if forloop.first %}active{% endif %}{% if fieldset.has_mandatory %} has-mandatory{% endif %}"
|
||||
id="{{ fieldset.title|slugify }}-tab"
|
||||
data-bs-toggle="tab"
|
||||
data-bs-target="#custom-{{ fieldset.title|slugify }}"
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-controls="custom-{{ fieldset.title|slugify }}"
|
||||
aria-selected="{% if forloop.first %}true{% else %}false{% endif %}">
|
||||
{{ fieldset.title }}
|
||||
{% if fieldset.has_mandatory %}<span class="mandatory-indicator">*</span>{% endif %}
|
||||
</button>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<div class="tab-content" id="myTabContent">
|
||||
{% for fieldset in form.get_fieldsets %}
|
||||
<div class="tab-pane fade my-2 {% if fieldset.hidden %}d-none{% endif %}{% if forloop.first %}show active{% endif %}"
|
||||
id="custom-{{ fieldset.title|slugify }}"
|
||||
role="tabpanel"
|
||||
aria-labelledby="custom-{{ fieldset.title|slugify }}-tab">
|
||||
{% for field in fieldset.fields %}
|
||||
{% with field=form|get_field:field %}{{ field.as_field_group }}{% endwith %}
|
||||
{% endfor %}
|
||||
|
|
@ -36,113 +74,70 @@
|
|||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% elif form %}
|
||||
{# Multiple fieldsets or auto-generated form - render with tabs #}
|
||||
<ul class="nav nav-tabs" id="myTab" role="tablist">
|
||||
{% for fieldset in form.get_fieldsets %}
|
||||
{% if not fieldset.hidden %}
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link {% if forloop.first %}active{% endif %}{% if fieldset.has_mandatory %} has-mandatory{% endif %}"
|
||||
id="{{ fieldset.title|slugify }}-tab"
|
||||
data-bs-toggle="tab"
|
||||
data-bs-target="#custom-{{ fieldset.title|slugify }}"
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-controls="custom-{{ fieldset.title|slugify }}"
|
||||
aria-selected="{% if forloop.first %}true{% else %}false{% endif %}">
|
||||
{{ fieldset.title }}
|
||||
{% if fieldset.has_mandatory %}<span class="mandatory-indicator">*</span>{% endif %}
|
||||
</button>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<div class="tab-content" id="myTabContent">
|
||||
{% for fieldset in form.get_fieldsets %}
|
||||
<div class="tab-pane fade my-2 {% if fieldset.hidden %}d-none{% endif %}{% if forloop.first %}show active{% endif %}"
|
||||
id="custom-{{ fieldset.title|slugify }}"
|
||||
role="tabpanel"
|
||||
aria-labelledby="custom-{{ fieldset.title|slugify }}-tab">
|
||||
{% for field in fieldset.fields %}
|
||||
{% with field=form|get_field:field %}{{ field.as_field_group }}{% endwith %}
|
||||
{% endfor %}
|
||||
{% for subfieldset in fieldset.fieldsets %}
|
||||
{% if subfieldset.fields %}
|
||||
<div>
|
||||
<h4 class="mt-3">{{ subfieldset.title }}</h4>
|
||||
{% for field in subfieldset.fields %}
|
||||
{% with field=form|get_field:field %}{{ field.as_field_group }}{% endwith %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if expert_form and not hide_expert_mode %}
|
||||
<div id="expert-form-container"
|
||||
class="expert-crd-form"
|
||||
style="{% if form %}display:none{% endif %}">
|
||||
{% if expert_form and expert_form.context %}{{ expert_form.context }}{% endif %}
|
||||
<ul class="nav nav-tabs" id="expertTab" role="tablist">
|
||||
{% for fieldset in expert_form.get_fieldsets %}
|
||||
{% if not fieldset.hidden %}
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link {% if forloop.first %}active{% endif %}{% if fieldset.has_mandatory %} has-mandatory{% endif %}"
|
||||
id="expert-{{ fieldset.title|slugify }}-tab"
|
||||
data-bs-toggle="tab"
|
||||
data-bs-target="#expert-{{ fieldset.title|slugify }}"
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-controls="expert-{{ fieldset.title|slugify }}"
|
||||
aria-selected="{% if forloop.first %}true{% else %}false{% endif %}">
|
||||
{{ fieldset.title }}
|
||||
{% if fieldset.has_mandatory %}<span class="mandatory-indicator">*</span>{% endif %}
|
||||
</button>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<div class="tab-content" id="expertTabContent">
|
||||
{% for fieldset in expert_form.get_fieldsets %}
|
||||
<div class="tab-pane fade my-2 {% if fieldset.hidden %}d-none{% endif %}{% if forloop.first %}show active{% endif %}"
|
||||
id="expert-{{ fieldset.title|slugify }}"
|
||||
role="tabpanel"
|
||||
aria-labelledby="expert-{{ fieldset.title|slugify }}-tab">
|
||||
{% for field in fieldset.fields %}
|
||||
{% with field=expert_form|get_field:field %}{{ field.as_field_group }}{% endwith %}
|
||||
{% endfor %}
|
||||
{% for subfieldset in fieldset.fieldsets %}
|
||||
{% if subfieldset.fields %}
|
||||
<div>
|
||||
<h4 class="mt-3">{{ subfieldset.title }}</h4>
|
||||
{% for field in subfieldset.fields %}
|
||||
{% with field=expert_form|get_field:field %}{{ field.as_field_group }}{% endwith %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if form %}
|
||||
<input type="hidden"
|
||||
name="active_form"
|
||||
id="active-form-input"
|
||||
value="custom">
|
||||
{% endif %}
|
||||
<div class="col-sm-12 d-flex justify-content-end">
|
||||
{# browser form validation fails when there are fields missing/invalid that are hidden #}
|
||||
<input class="btn btn-primary me-1 mb-1"
|
||||
type="submit"
|
||||
{% if form and expert_form %}formnovalidate{% endif %}
|
||||
value="{% if form_submit_label %}{{ form_submit_label }}{% else %}{% translate "Save" %}{% endif %}" />
|
||||
</div>
|
||||
{% if expert_form and not hide_expert_mode %}
|
||||
<div id="expert-form-container"
|
||||
class="expert-crd-form"
|
||||
style="{% if form %}display:none{% endif %}">
|
||||
{% if expert_form and expert_form.context %}{{ expert_form.context }}{% endif %}
|
||||
<ul class="nav nav-tabs" id="expertTab" role="tablist">
|
||||
{% for fieldset in expert_form.get_fieldsets %}
|
||||
{% if not fieldset.hidden %}
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link {% if forloop.first %}active{% endif %}{% if fieldset.has_mandatory %} has-mandatory{% endif %}"
|
||||
id="expert-{{ fieldset.title|slugify }}-tab"
|
||||
data-bs-toggle="tab"
|
||||
data-bs-target="#expert-{{ fieldset.title|slugify }}"
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-controls="expert-{{ fieldset.title|slugify }}"
|
||||
aria-selected="{% if forloop.first %}true{% else %}false{% endif %}">
|
||||
{{ fieldset.title }}
|
||||
{% if fieldset.has_mandatory %}<span class="mandatory-indicator">*</span>{% endif %}
|
||||
</button>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<div class="tab-content" id="expertTabContent">
|
||||
{% for fieldset in expert_form.get_fieldsets %}
|
||||
<div class="tab-pane fade my-2 {% if fieldset.hidden %}d-none{% endif %}{% if forloop.first %}show active{% endif %}"
|
||||
id="expert-{{ fieldset.title|slugify }}"
|
||||
role="tabpanel"
|
||||
aria-labelledby="expert-{{ fieldset.title|slugify }}-tab">
|
||||
{% for field in fieldset.fields %}
|
||||
{% with field=expert_form|get_field:field %}{{ field.as_field_group }}{% endwith %}
|
||||
{% endfor %}
|
||||
{% for subfieldset in fieldset.fieldsets %}
|
||||
{% if subfieldset.fields %}
|
||||
<div>
|
||||
<h4 class="mt-3">{{ subfieldset.title }}</h4>
|
||||
{% for field in subfieldset.fields %}
|
||||
{% with field=expert_form|get_field:field %}{{ field.as_field_group }}{% endwith %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% if form %}
|
||||
<input type="hidden"
|
||||
name="active_form"
|
||||
id="active-form-input"
|
||||
value="custom">
|
||||
{% endif %}
|
||||
<div class="col-sm-12 d-flex justify-content-end">
|
||||
{# browser form validation fails when there are fields missing/invalid that are hidden #}
|
||||
<input class="btn btn-primary me-1 mb-1"
|
||||
type="submit"
|
||||
{% if form and expert_form %}formnovalidate{% endif %}
|
||||
value="{% if form_submit_label %}{{ form_submit_label }}{% else %}{% translate "Save" %}{% endif %}" />
|
||||
</div>
|
||||
<script defer src="{% static 'js/bootstrap-tabs.js' %}"></script>
|
||||
{% if form and not hide_expert_mode %}
|
||||
<script defer src="{% static 'js/expert-mode.js' %}"></script>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue