diff --git a/netbox/netbox/forms/base.py b/netbox/netbox/forms/base.py index 632d5ecb1..57cfd1801 100644 --- a/netbox/netbox/forms/base.py +++ b/netbox/netbox/forms/base.py @@ -8,7 +8,7 @@ from django.utils.translation import gettext_lazy as _ from core.models import ObjectType from extras.choices import * from extras.models import CustomField, Tag -from utilities.forms import CSVModelForm +from utilities.forms import BulkEditForm, CSVModelForm from utilities.forms.fields import CSVModelMultipleChoiceField, DynamicModelMultipleChoiceField from utilities.forms.mixins import CheckLastUpdatedMixin from .mixins import CustomFieldsMixin, SavedFiltersMixin, TagsMixin @@ -100,7 +100,7 @@ class NetBoxModelImportForm(CSVModelForm, NetBoxModelForm): return customfield.to_form_field(for_csv_import=True) -class NetBoxModelBulkEditForm(CustomFieldsMixin, forms.Form): +class NetBoxModelBulkEditForm(CustomFieldsMixin, BulkEditForm): """ Base form for modifying multiple NetBox objects (of the same type) in bulk via the UI. Adds support for custom fields and adding/removing tags. @@ -108,9 +108,8 @@ class NetBoxModelBulkEditForm(CustomFieldsMixin, forms.Form): Attributes: fieldsets: An iterable of two-tuples which define a heading and field set to display per section of the rendered form (optional). If not defined, the all fields will be rendered as a single section. - nullable_fields: A list of field names indicating which fields support being set to null/empty """ - nullable_fields = () + fieldsets = None pk = forms.ModelMultipleChoiceField( queryset=None, # Set from self.model on init diff --git a/netbox/netbox/jobs.py b/netbox/netbox/jobs.py index fea5e9200..72743eaf4 100644 --- a/netbox/netbox/jobs.py +++ b/netbox/netbox/jobs.py @@ -212,6 +212,5 @@ class AsyncViewJob(JobRunner): ) notification.save() - # TODO: Waiting on fix for bug #19806 - # if errors: - # raise JobFailed() + if data.errors: + raise JobFailed() diff --git a/netbox/netbox/views/generic/bulk_views.py b/netbox/netbox/views/generic/bulk_views.py index c207c3d8d..686326881 100644 --- a/netbox/netbox/views/generic/bulk_views.py +++ b/netbox/netbox/views/generic/bulk_views.py @@ -27,6 +27,7 @@ from utilities.exceptions import AbortRequest, AbortTransaction, PermissionsViol from utilities.export import TableExport from utilities.forms import BulkRenameForm, ConfirmationForm, restrict_form_fields from utilities.forms.bulk_import import BulkImportForm +from utilities.forms.mixins import BackgroundJobMixin from utilities.htmx import htmx_partial from utilities.jobs import AsyncJobData, is_background_request, process_request_as_job from utilities.permissions import get_permission_for_model @@ -513,12 +514,7 @@ class BulkImportView(GetReturnURLMixin, BaseMultiObjectView): count=len(form.cleaned_data['data']), object_type=model._meta.verbose_name_plural, ) - if job := process_request_as_job(self.__class__, request, name=job_name): - msg = _('Created background job {job.pk}: {job.name}').format( - url=job.get_absolute_url(), - job=job - ) - messages.info(request, mark_safe(msg)) + if process_request_as_job(self.__class__, request, name=job_name): return redirect(redirect_url) try: @@ -712,6 +708,16 @@ class BulkEditView(GetReturnURLMixin, BaseMultiObjectView): if '_apply' in request.POST: 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['background_job']: + job_name = _('Bulk edit {count} {object_type}').format( + count=len(form.cleaned_data['pk']), + object_type=model._meta.verbose_name_plural, + ) + if process_request_as_job(self.__class__, request, name=job_name): + return redirect(self.get_return_url(request)) + try: with transaction.atomic(using=router.db_for_write(model)): updated_objects = self._update_objects(form, request) @@ -721,6 +727,16 @@ class BulkEditView(GetReturnURLMixin, BaseMultiObjectView): if object_count != len(updated_objects): raise PermissionsViolation + # If this request was executed via a background job, return the raw data for logging + if is_background_request(request): + return AsyncJobData( + log=[ + _('Updated {object}').format(object=str(obj)) + for obj in updated_objects + ], + errors=form.errors + ) + if updated_objects: msg = f'Updated {len(updated_objects)} {model._meta.verbose_name_plural}' logger.info(msg) @@ -876,7 +892,7 @@ class BulkDeleteView(GetReturnURLMixin, BaseMultiObjectView): """ Provide a standard bulk delete form if none has been specified for the view """ - class BulkDeleteForm(ConfirmationForm): + class BulkDeleteForm(BackgroundJobMixin, ConfirmationForm): pk = ModelMultipleChoiceField(queryset=self.queryset, widget=MultipleHiddenInput) return BulkDeleteForm @@ -908,6 +924,15 @@ class BulkDeleteView(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['background_job']: + job_name = _('Bulk delete {count} {object_type}').format( + count=len(form.cleaned_data['pk']), + object_type=model._meta.verbose_name_plural, + ) + if process_request_as_job(self.__class__, request, name=job_name): + return redirect(self.get_return_url(request)) + # Delete objects queryset = self.queryset.filter(pk__in=pk_list) deleted_count = queryset.count() @@ -929,6 +954,16 @@ class BulkDeleteView(GetReturnURLMixin, BaseMultiObjectView): messages.error(request, mark_safe(e.message)) return redirect(self.get_return_url(request)) + # If this request was executed via a background job, return the raw data for logging + if is_background_request(request): + return AsyncJobData( + log=[ + _('Deleted {object}').format(object=str(obj)) + for obj in queryset + ], + errors=form.errors + ) + msg = _("Deleted {count} {object_type}").format( count=deleted_count, object_type=model._meta.verbose_name_plural diff --git a/netbox/templates/generic/bulk_delete.html b/netbox/templates/generic/bulk_delete.html index 4e3eecd8e..594efff63 100644 --- a/netbox/templates/generic/bulk_delete.html +++ b/netbox/templates/generic/bulk_delete.html @@ -1,7 +1,8 @@ {% extends 'generic/_base.html' %} +{% load form_helpers %} {% load helpers %} -{% load render_table from django_tables2 %} {% load i18n %} +{% load render_table from django_tables2 %} {% comment %} Blocks: @@ -58,13 +59,23 @@ Context: