Closes #17789: Use a single scope field for VLANGroup bulk edit

This commit is contained in:
Jeremy Stretch 2024-10-16 16:05:01 -04:00
parent 9f7743e5da
commit 6a316df787
3 changed files with 90 additions and 132 deletions

View File

@ -1,22 +1,23 @@
from django import forms from django import forms
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ObjectDoesNotExist
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from dcim.models import Location, Rack, Region, Site, SiteGroup from dcim.models import Region, Site, SiteGroup
from ipam.choices import * from ipam.choices import *
from ipam.constants import * from ipam.constants import *
from ipam.models import * from ipam.models import *
from ipam.models import ASN from ipam.models import ASN
from netbox.forms import NetBoxModelBulkEditForm from netbox.forms import NetBoxModelBulkEditForm
from tenancy.models import Tenant from tenancy.models import Tenant
from utilities.forms import add_blank_choice from utilities.forms import add_blank_choice, get_field_value
from utilities.forms.fields import ( from utilities.forms.fields import (
CommentField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, NumericArrayField, CommentField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, NumericArrayField,
NumericRangeArrayField, NumericRangeArrayField,
) )
from utilities.forms.rendering import FieldSet from utilities.forms.rendering import FieldSet
from utilities.forms.widgets import BulkEditNullBooleanSelect from utilities.forms.widgets import BulkEditNullBooleanSelect, HTMXSelect
from virtualization.models import Cluster, ClusterGroup from utilities.templatetags.builtins.filters import bettertitle
__all__ = ( __all__ = (
'AggregateBulkEditForm', 'AggregateBulkEditForm',
@ -429,62 +430,17 @@ class VLANGroupBulkEditForm(NetBoxModelBulkEditForm):
required=False required=False
) )
scope_type = ContentTypeChoiceField( scope_type = ContentTypeChoiceField(
label=_('Scope type'),
queryset=ContentType.objects.filter(model__in=VLANGROUP_SCOPE_TYPES), queryset=ContentType.objects.filter(model__in=VLANGROUP_SCOPE_TYPES),
required=False widget=HTMXSelect(method='post', attrs={'hx-select': '#form_fields'}),
)
scope_id = forms.IntegerField(
required=False, required=False,
widget=forms.HiddenInput() label=_('Scope type')
) )
region = DynamicModelChoiceField( scope = DynamicModelChoiceField(
label=_('Region'), label=_('Scope'),
queryset=Region.objects.all(), queryset=Site.objects.none(), # Initial queryset
required=False
)
sitegroup = DynamicModelChoiceField(
queryset=SiteGroup.objects.all(),
required=False, required=False,
label=_('Site group') disabled=True,
) selector=True
site = DynamicModelChoiceField(
label=_('Site'),
queryset=Site.objects.all(),
required=False,
query_params={
'region_id': '$region',
'group_id': '$sitegroup',
}
)
location = DynamicModelChoiceField(
label=_('Location'),
queryset=Location.objects.all(),
required=False,
query_params={
'site_id': '$site',
}
)
rack = DynamicModelChoiceField(
label=_('Rack'),
queryset=Rack.objects.all(),
required=False,
query_params={
'site_id': '$site',
'location_id': '$location',
}
)
clustergroup = DynamicModelChoiceField(
queryset=ClusterGroup.objects.all(),
required=False,
label=_('Cluster group')
)
cluster = DynamicModelChoiceField(
label=_('Cluster'),
queryset=Cluster.objects.all(),
required=False,
query_params={
'group_id': '$clustergroup',
}
) )
vid_ranges = NumericRangeArrayField( vid_ranges = NumericRangeArrayField(
label=_('VLAN ID ranges'), label=_('VLAN ID ranges'),
@ -494,24 +450,23 @@ class VLANGroupBulkEditForm(NetBoxModelBulkEditForm):
model = VLANGroup model = VLANGroup
fieldsets = ( fieldsets = (
FieldSet('site', 'vid_ranges', 'description'), FieldSet('site', 'vid_ranges', 'description'),
FieldSet( FieldSet('scope_type', 'scope', name=_('Scope')),
'scope_type', 'region', 'sitegroup', 'site', 'location', 'rack', 'clustergroup', 'cluster', name=_('Scope')
),
) )
nullable_fields = ('description',) nullable_fields = ('description', 'scope')
def clean(self): def __init__(self, *args, **kwargs):
super().clean() super().__init__(*args, **kwargs)
# Assign scope based on scope_type if scope_type_id := get_field_value(self, 'scope_type'):
if self.cleaned_data.get('scope_type'): try:
scope_field = self.cleaned_data['scope_type'].model scope_type = ContentType.objects.get(pk=scope_type_id)
if scope_obj := self.cleaned_data.get(scope_field): model = scope_type.model_class()
self.cleaned_data['scope_id'] = scope_obj.pk self.fields['scope'].queryset = model.objects.all()
self.changed_data.append('scope_id') self.fields['scope'].widget.attrs['selector'] = model._meta.label_lower
else: self.fields['scope'].disabled = False
self.cleaned_data.pop('scope_type') self.fields['scope'].label = _(bettertitle(model._meta.verbose_name))
self.changed_data.remove('scope_type') except ObjectDoesNotExist:
pass
class VLANBulkEditForm(NetBoxModelBulkEditForm): class VLANBulkEditForm(NetBoxModelBulkEditForm):

View File

@ -3,7 +3,7 @@ import re
from copy import deepcopy from copy import deepcopy
from django.contrib import messages from django.contrib import messages
from django.contrib.contenttypes.fields import GenericRel from django.contrib.contenttypes.fields import GenericForeignKey, GenericRel
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import FieldDoesNotExist, ObjectDoesNotExist, ValidationError from django.core.exceptions import FieldDoesNotExist, ObjectDoesNotExist, ValidationError
from django.db import transaction, IntegrityError from django.db import transaction, IntegrityError
@ -576,7 +576,10 @@ class BulkEditView(GetReturnURLMixin, BaseMultiObjectView):
for name, model_field in model_fields.items(): for name, model_field in model_fields.items():
# Handle nullification # Handle nullification
if name in form.nullable_fields and name in nullified_fields: if name in form.nullable_fields and name in nullified_fields:
setattr(obj, name, None if model_field.null else '') if type(model_field) is GenericForeignKey:
setattr(obj, name, None)
else:
setattr(obj, name, None if model_field.null else '')
# Normal fields # Normal fields
elif name in form.changed_data: elif name in form.changed_data:
setattr(obj, name, form.cleaned_data[name]) setattr(obj, name, form.cleaned_data[name])
@ -688,7 +691,7 @@ class BulkEditView(GetReturnURLMixin, BaseMultiObjectView):
logger.debug("Form validation failed") logger.debug("Form validation failed")
else: else:
form = self.form(initial=initial_data) form = self.form(request.POST, initial=initial_data)
restrict_form_fields(form, request.user) restrict_form_fields(form, request.user)
# Retrieve objects being edited # Retrieve objects being edited

View File

@ -42,71 +42,71 @@ Context:
{# Edit form #} {# Edit form #}
<div class="tab-pane show active" id="edit-form" role="tabpanel" aria-labelledby="edit-form-tab"> <div class="tab-pane show active" id="edit-form" role="tabpanel" aria-labelledby="edit-form-tab">
<form action="" method="post" class="form form-horizontal mt-5"> <form action="" method="post" class="form form-horizontal mt-5">
<div id="form_fields" hx-disinherit="hx-select hx-swap">
{% csrf_token %} {% csrf_token %}
{% if request.POST.return_url %} {% if request.POST.return_url %}
<input type="hidden" name="return_url" value="{{ request.POST.return_url }}" /> <input type="hidden" name="return_url" value="{{ request.POST.return_url }}" />
{% endif %} {% endif %}
{% for field in form.hidden_fields %} {% for field in form.hidden_fields %}
{{ field }} {{ field }}
{% endfor %}
{% if form.fieldsets %}
{# Render grouped fields according to declared fieldsets #}
{% for fieldset in form.fieldsets %}
{% render_fieldset form fieldset %}
{% endfor %} {% endfor %}
{# Render tag add/remove fields #} {% if form.fieldsets %}
{% if form.add_tags and form.remove_tags %}
<div class="field-group mb-5"> {# Render grouped fields according to declared fieldsets #}
<div class="row"> {% for fieldset in form.fieldsets %}
<h2 class="col-9 offset-3">{% trans "Tags" %}</h2> {% render_fieldset form fieldset %}
{% endfor %}
{# Render tag add/remove fields #}
{% if form.add_tags and form.remove_tags %}
<div class="field-group mb-5">
<div class="row">
<h2 class="col-9 offset-3">{% trans "Tags" %}</h2>
</div>
{% render_field form.add_tags %}
{% render_field form.remove_tags %}
</div> </div>
{% render_field form.add_tags %}
{% render_field form.remove_tags %}
</div>
{% endif %}
{# Render custom fields #}
{% if form.custom_fields %}
<div class="field-group mb-5">
<div class="row">
<h2 class="col-9 offset-3">{% trans "Custom Fields" %}</h2>
</div>
{% render_custom_fields form %}
</div>
{% endif %}
{# Render comments #}
{% if form.comments %}
<div class="field-group mb-5">
<div class="row">
<h2 class="col-9 offset-3">{% trans "Comments" %}</h2>
</div>
{% render_field form.comments bulk_nullable=True %}
</div>
{% endif %}
{% else %}
{# Render all fields #}
{% for field in form.visible_fields %}
{% if field.name in form.nullable_fields %}
{% render_field field bulk_nullable=True %}
{% else %}
{% render_field field %}
{% endif %} {% endif %}
{% endfor %}
{% endif %} {# Render custom fields #}
{% if form.custom_fields %}
<div class="field-group mb-5">
<div class="row">
<h2 class="col-9 offset-3">{% trans "Custom Fields" %}</h2>
</div>
{% render_custom_fields form %}
</div>
{% endif %}
<div class="btn-float-group-right"> {# Render comments #}
<a href="{{ return_url }}" class="btn btn-outline-secondary btn-float">{% trans "Cancel" %}</a> {% if form.comments %}
<button type="submit" name="_apply" class="btn btn-primary">{% trans "Apply" %}</button> <div class="field-group mb-5">
<div class="row">
<h2 class="col-9 offset-3">{% trans "Comments" %}</h2>
</div>
{% render_field form.comments bulk_nullable=True %}
</div>
{% endif %}
{% else %}
{# Render all fields #}
{% for field in form.visible_fields %}
{% if field.name in form.nullable_fields %}
{% render_field field bulk_nullable=True %}
{% else %}
{% render_field field %}
{% endif %}
{% endfor %}
{% endif %}
<div class="btn-float-group-right">
<a href="{{ return_url }}" class="btn btn-outline-secondary btn-float">{% trans "Cancel" %}</a>
<button type="submit" name="_apply" class="btn btn-primary">{% trans "Apply" %}</button>
</div>
</div> </div>
</form> </form>
</div> </div>