From 84670af18b388594a28117be1d55552387327e4a Mon Sep 17 00:00:00 2001 From: Arthur Date: Thu, 26 Mar 2026 09:56:21 -0700 Subject: [PATCH 1/4] #20162 allow background job when adding components to devices in bulk --- netbox/dcim/forms/bulk_create.py | 3 ++- netbox/netbox/views/generic/bulk_views.py | 16 ++++++++++++++++ netbox/templates/generic/bulk_add_component.html | 11 ++++++++++- netbox/virtualization/forms/bulk_create.py | 3 ++- 4 files changed, 30 insertions(+), 3 deletions(-) diff --git a/netbox/dcim/forms/bulk_create.py b/netbox/dcim/forms/bulk_create.py index a13b2bf38..c6ac6271e 100644 --- a/netbox/dcim/forms/bulk_create.py +++ b/netbox/dcim/forms/bulk_create.py @@ -6,6 +6,7 @@ from extras.models import Tag from netbox.forms.mixins import CustomFieldsMixin from utilities.forms import form_from_model from utilities.forms.fields import DynamicModelMultipleChoiceField, ExpandableNameField +from utilities.forms.mixins import BackgroundJobMixin from .object_create import ComponentCreateForm @@ -27,7 +28,7 @@ __all__ = ( # Device components # -class DeviceBulkAddComponentForm(CustomFieldsMixin, ComponentCreateForm): +class DeviceBulkAddComponentForm(BackgroundJobMixin, CustomFieldsMixin, ComponentCreateForm): pk = forms.ModelMultipleChoiceField( queryset=Device.objects.all(), widget=forms.MultipleHiddenInput() diff --git a/netbox/netbox/views/generic/bulk_views.py b/netbox/netbox/views/generic/bulk_views.py index 25756e212..243036091 100644 --- a/netbox/netbox/views/generic/bulk_views.py +++ b/netbox/netbox/views/generic/bulk_views.py @@ -1137,8 +1137,18 @@ class BulkComponentCreateView(GetReturnURLMixin, BaseMultiObjectView): if form.is_valid(): logger.debug("Form validation was successful") + # If indicated, defer this request to a background job & redirect the user + if form.cleaned_data.get('background_job'): + job_name = _('Bulk add {count} {object_type}').format( + count=len(form.cleaned_data['pk']), + object_type=model_name, + ) + if process_request_as_job(self.__class__, request, name=job_name): + return redirect(self.get_return_url(request)) + new_components = [] data = deepcopy(form.cleaned_data) + data.pop('background_job', None) replication_data = { field: data.pop(field) for field in form.replication_fields } @@ -1189,6 +1199,12 @@ class BulkComponentCreateView(GetReturnURLMixin, BaseMultiObjectView): parent_model_name ) logger.info(msg) + + # Handle background job + if is_background_request(request): + request.job.logger.info(msg) + return None + messages.success(request, msg) return redirect(self.get_return_url(request)) diff --git a/netbox/templates/generic/bulk_add_component.html b/netbox/templates/generic/bulk_add_component.html index 72502f441..078a45930 100644 --- a/netbox/templates/generic/bulk_add_component.html +++ b/netbox/templates/generic/bulk_add_component.html @@ -58,10 +58,19 @@ Context:

{{ model_name|title }} {% trans "to Add" %}

{% for field in form.visible_fields %} - {% render_field field %} + {% if form.meta_fields and field.name in form.meta_fields %} + {% else %} + {% render_field field %} + {% endif %} {% endfor %}
+ {# Meta fields #} + {% if form.background_job %} +
+ {% render_field form.background_job %} +
+ {% endif %}
{% trans "Cancel" %} diff --git a/netbox/virtualization/forms/bulk_create.py b/netbox/virtualization/forms/bulk_create.py index 078fb2f43..a1d7c5e54 100644 --- a/netbox/virtualization/forms/bulk_create.py +++ b/netbox/virtualization/forms/bulk_create.py @@ -3,6 +3,7 @@ from django.utils.translation import gettext_lazy as _ from utilities.forms import form_from_model from utilities.forms.fields import ExpandableNameField +from utilities.forms.mixins import BackgroundJobMixin from virtualization.models import VirtualDisk, VirtualMachine, VMInterface __all__ = ( @@ -11,7 +12,7 @@ __all__ = ( ) -class VirtualMachineBulkAddComponentForm(forms.Form): +class VirtualMachineBulkAddComponentForm(BackgroundJobMixin, forms.Form): pk = forms.ModelMultipleChoiceField( queryset=VirtualMachine.objects.all(), widget=forms.MultipleHiddenInput() From 8a58d760faa135a472f16e9e9a0278c109a83bab Mon Sep 17 00:00:00 2001 From: Arthur Date: Thu, 26 Mar 2026 13:25:49 -0700 Subject: [PATCH 2/4] cleanup --- netbox/templates/generic/bulk_add_component.html | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/netbox/templates/generic/bulk_add_component.html b/netbox/templates/generic/bulk_add_component.html index 078a45930..95f323726 100644 --- a/netbox/templates/generic/bulk_add_component.html +++ b/netbox/templates/generic/bulk_add_component.html @@ -58,8 +58,7 @@ Context:

{{ model_name|title }} {% trans "to Add" %}

{% for field in form.visible_fields %} - {% if form.meta_fields and field.name in form.meta_fields %} - {% else %} + {% if not form.meta_fields or field.name not in form.meta_fields %} {% render_field field %} {% endif %} {% endfor %} From 3ec055168067cacb34b6a9393609924a7c6b95bb Mon Sep 17 00:00:00 2001 From: Arthur Date: Thu, 26 Mar 2026 13:37:40 -0700 Subject: [PATCH 3/4] cleanup --- netbox/netbox/views/generic/bulk_views.py | 25 +++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/netbox/netbox/views/generic/bulk_views.py b/netbox/netbox/views/generic/bulk_views.py index 243036091..932421fe9 100644 --- a/netbox/netbox/views/generic/bulk_views.py +++ b/netbox/netbox/views/generic/bulk_views.py @@ -1138,10 +1138,10 @@ class BulkComponentCreateView(GetReturnURLMixin, BaseMultiObjectView): logger.debug("Form validation was successful") # If indicated, defer this request to a background job & redirect the user - if form.cleaned_data.get('background_job'): + if form.cleaned_data['background_job']: job_name = _('Bulk add {count} {object_type}').format( count=len(form.cleaned_data['pk']), - object_type=model_name, + object_type=self.queryset.model._meta.verbose_name_plural, ) if process_request_as_job(self.__class__, request, name=job_name): return redirect(self.get_return_url(request)) @@ -1176,7 +1176,10 @@ class BulkComponentCreateView(GetReturnURLMixin, BaseMultiObjectView): else: for field, errors in component_form.errors.as_data().items(): for e in errors: - form.add_error(field, '{}: {}'.format(obj, ', '.join(e))) + err_msg = '{}: {}'.format(obj, ', '.join(e)) + form.add_error(field, err_msg) + if is_background_request(request): + request.job.logger.error(err_msg) # Enforce object-level permissions component_ids = [obj.pk for obj in new_components] @@ -1185,18 +1188,24 @@ class BulkComponentCreateView(GetReturnURLMixin, BaseMultiObjectView): except IntegrityError: clear_events.send(sender=self) + if is_background_request(request): + request.job.logger.error("An integrity error occurred while creating components") + raise JobFailed except (AbortRequest, PermissionsViolation) as e: logger.debug(e.message) form.add_error(None, e.message) clear_events.send(sender=self) + if is_background_request(request): + request.job.logger.error(e.message) + raise JobFailed if not form.errors: - msg = "Added {} {} to {} {}.".format( - len(new_components), - model_name, - len(form.cleaned_data['pk']), - parent_model_name + msg = _("Added {count} {component} to {parent_count} {parent}.").format( + count=len(new_components), + component=model_name, + parent_count=len(form.cleaned_data['pk']), + parent=parent_model_name, ) logger.info(msg) From 9bc66ee0bf54bf438422cc6cde860d54006d593e Mon Sep 17 00:00:00 2001 From: Arthur Date: Thu, 26 Mar 2026 15:00:52 -0700 Subject: [PATCH 4/4] cleanup --- netbox/netbox/views/generic/bulk_views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/netbox/views/generic/bulk_views.py b/netbox/netbox/views/generic/bulk_views.py index 932421fe9..be3cc619e 100644 --- a/netbox/netbox/views/generic/bulk_views.py +++ b/netbox/netbox/views/generic/bulk_views.py @@ -1189,7 +1189,7 @@ class BulkComponentCreateView(GetReturnURLMixin, BaseMultiObjectView): except IntegrityError: clear_events.send(sender=self) if is_background_request(request): - request.job.logger.error("An integrity error occurred while creating components") + request.job.logger.error(_("An integrity error occurred while creating components")) raise JobFailed except (AbortRequest, PermissionsViolation) as e: