mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-23 04:22:01 -06:00
Closes #17789: Use a single scope field for VLANGroup bulk edit
This commit is contained in:
parent
9f7743e5da
commit
6a316df787
@ -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):
|
||||||
|
@ -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
|
||||||
|
@ -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>
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user