feat(ipam): Add HTMX support to prefix bulk add form

Enable dynamic form updates in the prefix bulk add view by introducing
HTMX partial rendering. Inherit from PrefixForm to support scope and
VLAN fields, and add htmx_template_name for efficient field updates.
This commit is contained in:
Martin Hauser
2026-03-13 15:10:46 +01:00
parent 639a739b5b
commit 775d6aa936
6 changed files with 53 additions and 24 deletions

View File

@@ -250,31 +250,22 @@ class PrefixForm(TenancyForm, ScopedForm, PrimaryModelForm):
self.fields['vlan'].widget.attrs.pop('data-dynamic-params', None)
class PrefixBulkAddForm(TenancyForm, NetBoxModelForm):
vrf = DynamicModelChoiceField(
queryset=VRF.objects.all(),
required=False,
label=_('VRF')
)
role = DynamicModelChoiceField(
label=_('Role'),
queryset=Role.objects.all(),
required=False,
quick_add=True
)
class PrefixBulkAddForm(PrefixForm):
"""
Subclass of PrefixForm for bulk creation. The prefix field is inherited
but excluded from fieldsets — it is populated programmatically by BulkCreateView
from the expanded pattern.
"""
fieldsets = (
FieldSet('status', 'role', 'vrf', 'is_pool', 'mark_utilized', 'description', 'tags', name=_('Prefix')),
FieldSet(
'status', 'vrf', 'role', 'is_pool', 'mark_utilized', 'description', 'tags', name=_('Prefix')
),
FieldSet('scope_type', 'scope', name=_('Scope')),
FieldSet('vlan', name=_('VLAN Assignment')),
FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
)
class Meta:
model = Prefix
fields = [
'prefix', 'vrf', 'status', 'role', 'is_pool', 'mark_utilized', 'description', 'tenant_group', 'tenant',
'tags',
]
class IPRangeForm(TenancyForm, PrimaryModelForm):
vrf = DynamicModelChoiceField(

View File

@@ -225,6 +225,7 @@ class BulkCreateView(GetReturnURLMixin, BaseMultiObjectView):
form = None
model_form = None
pattern_target = ''
htmx_template_name = 'htmx/bulk_add_form.html'
def get_required_permission(self):
return get_permission_for_model(self.queryset.model, 'add')
@@ -280,6 +281,12 @@ class BulkCreateView(GetReturnURLMixin, BaseMultiObjectView):
form = self.form()
model_form = self.model_form(initial=initial)
# HTMX partial: only re-render the model form fields
if htmx_partial(request):
return render(request, self.htmx_template_name, {
'model_form': model_form,
})
return render(request, self.template_name, self._get_context(request, form, model_form))
def post(self, request):
@@ -288,6 +295,12 @@ class BulkCreateView(GetReturnURLMixin, BaseMultiObjectView):
form = self.form(request.POST)
model_form = self.model_form(request.POST)
# HTMX partial: only re-render the model form fields
if htmx_partial(request):
return render(request, self.htmx_template_name, {
'model_form': model_form,
})
if form.is_valid():
logger.debug("Form validation was successful")

View File

@@ -20,14 +20,16 @@
</ul>
{% endblock %}
{% block form %}
{% block pre_form_fields %}
<div class="field-group my-5">
<div class="row">
<h2 class="col-9 offset-3">{% trans "Pattern" %}</h2>
</div>
{% render_field form.pattern %}
</div>
{% endblock pre_form_fields %}
{% block form %}
{% if model_form.fieldsets %}
{% for fieldset in model_form.fieldsets %}
{% render_fieldset model_form fieldset %}
@@ -46,4 +48,4 @@
{% render_custom_fields model_form %}
</div>
{% endif %}
{% endblock %}
{% endblock form %}

View File

@@ -61,6 +61,7 @@ Context:
<form action="" method="post" enctype="multipart/form-data" class="object-edit mt-5">
{% csrf_token %}
{% block pre_form_fields %}{% endblock pre_form_fields %}
<div id="form_fields" hx-disinherit="hx-select hx-swap">
{% block form %}
{% include 'htmx/form.html' %}

View File

@@ -0,0 +1,22 @@
{% load helpers %}
{% load form_helpers %}
{% load i18n %}
{% if model_form.fieldsets %}
{% for fieldset in model_form.fieldsets %}
{% render_fieldset model_form fieldset %}
{% endfor %}
{% else %}
<div class="field-group my-5">
{% render_form model_form %}
</div>
{% endif %}
{% if model_form.custom_fields %}
<div class="field-group my-5">
<div class="row">
<h2 class="col-9 offset-3">{% trans "Custom Fields" %}</h2>
</div>
{% render_custom_fields model_form %}
</div>
{% endif %}

View File

@@ -3,8 +3,8 @@
<ul class="nav nav-tabs">
<li class="nav-item">
<a href="{% url 'ipam:prefix_add' %}{% querystring request %}" class="nav-link {% if active_tab == 'add' %}active{% endif %}">
{% if object.pk %}{% trans "Edit" %}{% else %}{% trans "Create" %}{% endif %}
<a href="{% if object.pk %}{% url 'ipam:prefix_edit' pk=object.pk %}{% else %}{% url 'ipam:prefix_add' %}{% querystring request %}{% endif %}" class="nav-link {% if active_tab == 'add' %}active{% endif %}">
{% if object.pk %}{% trans "Prefix" %}{% else %}{% trans "Create" %}{% endif %}
</a>
</li>
{% if not object.pk %}