diff --git a/netbox/ipam/forms/bulk_edit.py b/netbox/ipam/forms/bulk_edit.py index f4a7eabb7d..79debd0ed6 100644 --- a/netbox/ipam/forms/bulk_edit.py +++ b/netbox/ipam/forms/bulk_edit.py @@ -1,22 +1,23 @@ from django import forms from django.contrib.contenttypes.models import ContentType +from django.core.exceptions import ObjectDoesNotExist 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.constants import * from ipam.models import * from ipam.models import ASN from netbox.forms import NetBoxModelBulkEditForm 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 ( CommentField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, NumericArrayField, NumericRangeArrayField, ) from utilities.forms.rendering import FieldSet -from utilities.forms.widgets import BulkEditNullBooleanSelect -from virtualization.models import Cluster, ClusterGroup +from utilities.forms.widgets import BulkEditNullBooleanSelect, HTMXSelect +from utilities.templatetags.builtins.filters import bettertitle __all__ = ( 'AggregateBulkEditForm', @@ -429,62 +430,17 @@ class VLANGroupBulkEditForm(NetBoxModelBulkEditForm): required=False ) scope_type = ContentTypeChoiceField( - label=_('Scope type'), queryset=ContentType.objects.filter(model__in=VLANGROUP_SCOPE_TYPES), - required=False - ) - scope_id = forms.IntegerField( - required=False, - widget=forms.HiddenInput() - ) - region = DynamicModelChoiceField( - label=_('Region'), - queryset=Region.objects.all(), - required=False - ) - sitegroup = DynamicModelChoiceField( - queryset=SiteGroup.objects.all(), - required=False, - label=_('Site group') - ) - 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(), + widget=HTMXSelect(method='post', attrs={'hx-select': '#form_fields'}), 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', - } + label=_('Scope type') ) - clustergroup = DynamicModelChoiceField( - queryset=ClusterGroup.objects.all(), + scope = DynamicModelChoiceField( + label=_('Scope'), + queryset=Site.objects.none(), # Initial queryset required=False, - label=_('Cluster group') - ) - cluster = DynamicModelChoiceField( - label=_('Cluster'), - queryset=Cluster.objects.all(), - required=False, - query_params={ - 'group_id': '$clustergroup', - } + disabled=True, + selector=True ) vid_ranges = NumericRangeArrayField( label=_('VLAN ID ranges'), @@ -494,24 +450,23 @@ class VLANGroupBulkEditForm(NetBoxModelBulkEditForm): model = VLANGroup fieldsets = ( FieldSet('site', 'vid_ranges', 'description'), - FieldSet( - 'scope_type', 'region', 'sitegroup', 'site', 'location', 'rack', 'clustergroup', 'cluster', name=_('Scope') - ), - ) - nullable_fields = ('description',) - - def clean(self): - super().clean() - - # Assign scope based on scope_type - if self.cleaned_data.get('scope_type'): - scope_field = self.cleaned_data['scope_type'].model - if scope_obj := self.cleaned_data.get(scope_field): - self.cleaned_data['scope_id'] = scope_obj.pk - self.changed_data.append('scope_id') - else: - self.cleaned_data.pop('scope_type') - self.changed_data.remove('scope_type') + FieldSet('scope_type', 'scope', name=_('Scope')), + ) + nullable_fields = ('description', 'scope') + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + if scope_type_id := get_field_value(self, 'scope_type'): + try: + scope_type = ContentType.objects.get(pk=scope_type_id) + model = scope_type.model_class() + self.fields['scope'].queryset = model.objects.all() + self.fields['scope'].widget.attrs['selector'] = model._meta.label_lower + self.fields['scope'].disabled = False + self.fields['scope'].label = _(bettertitle(model._meta.verbose_name)) + except ObjectDoesNotExist: + pass class VLANBulkEditForm(NetBoxModelBulkEditForm): diff --git a/netbox/netbox/views/generic/bulk_views.py b/netbox/netbox/views/generic/bulk_views.py index d7d28b95f5..d8115726ce 100644 --- a/netbox/netbox/views/generic/bulk_views.py +++ b/netbox/netbox/views/generic/bulk_views.py @@ -3,7 +3,7 @@ from copy import deepcopy 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.core.exceptions import FieldDoesNotExist, ObjectDoesNotExist, ValidationError from django.db import transaction, IntegrityError @@ -576,7 +576,10 @@ def _update_objects(self, form, request): for name, model_field in model_fields.items(): # Handle nullification 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 elif name in form.changed_data: setattr(obj, name, form.cleaned_data[name]) @@ -688,7 +691,7 @@ def post(self, request, **kwargs): logger.debug("Form validation failed") else: - form = self.form(initial=initial_data) + form = self.form(request.POST, initial=initial_data) restrict_form_fields(form, request.user) # Retrieve objects being edited diff --git a/netbox/templates/generic/bulk_edit.html b/netbox/templates/generic/bulk_edit.html index 4b4d5aeced..8c4d305eca 100644 --- a/netbox/templates/generic/bulk_edit.html +++ b/netbox/templates/generic/bulk_edit.html @@ -42,71 +42,71 @@ {# Edit form #}
- - {% csrf_token %} - {% if request.POST.return_url %} - - {% endif %} - {% for field in form.hidden_fields %} - {{ field }} - {% endfor %} - - {% if form.fieldsets %} - - {# Render grouped fields according to declared fieldsets #} - {% for fieldset in form.fieldsets %} - {% render_fieldset form fieldset %} +
+ {% csrf_token %} + {% if request.POST.return_url %} + + {% endif %} + {% for field in form.hidden_fields %} + {{ field }} {% endfor %} - {# Render tag add/remove fields #} - {% if form.add_tags and form.remove_tags %} -
-
-

{% trans "Tags" %}

+ {% if form.fieldsets %} + + {# Render grouped fields according to declared fieldsets #} + {% for fieldset in form.fieldsets %} + {% render_fieldset form fieldset %} + {% endfor %} + + {# Render tag add/remove fields #} + {% if form.add_tags and form.remove_tags %} +
+
+

{% trans "Tags" %}

+
+ {% render_field form.add_tags %} + {% render_field form.remove_tags %}
- {% render_field form.add_tags %} - {% render_field form.remove_tags %} -
- {% endif %} + {% endif %} - {# Render custom fields #} - {% if form.custom_fields %} -
-
-

{% trans "Custom Fields" %}

+ {# Render custom fields #} + {% if form.custom_fields %} +
+
+

{% trans "Custom Fields" %}

+
+ {% render_custom_fields form %}
- {% render_custom_fields form %} -
- {% endif %} + {% endif %} - {# Render comments #} - {% if form.comments %} -
-
-

{% trans "Comments" %}

+ {# Render comments #} + {% if form.comments %} +
+
+

{% trans "Comments" %}

+
+ {% render_field form.comments bulk_nullable=True %}
- {% render_field form.comments bulk_nullable=True %} -
- {% endif %} + {% endif %} - {% else %} + {% 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 %} + {# 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 %} + {% endif %} -
- {% trans "Cancel" %} - +
+ {% trans "Cancel" %} + +
-