Enable background jobs for bulk edit & bulk delete

This commit is contained in:
Jeremy Stretch 2025-07-16 13:38:49 -04:00
parent 27a7263f7c
commit 76e0ee837b
4 changed files with 65 additions and 7 deletions

View File

@ -2,6 +2,7 @@ import logging
import re
from copy import deepcopy
from django import forms
from django.contrib import messages
from django.contrib.contenttypes.fields import GenericForeignKey, GenericRel
from django.contrib.contenttypes.models import ContentType
@ -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}: <a href="{url}">{job.name}</a>').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)
@ -878,6 +894,11 @@ class BulkDeleteView(GetReturnURLMixin, BaseMultiObjectView):
"""
class BulkDeleteForm(ConfirmationForm):
pk = ModelMultipleChoiceField(queryset=self.queryset, widget=MultipleHiddenInput)
background_job = forms.BooleanField(
label=_('Background job'),
help_text=_("Process as a job to edit objects in the background"),
required=False,
)
return BulkDeleteForm
@ -908,6 +929,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 +959,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

View File

@ -1,4 +1,5 @@
{% extends 'generic/_base.html' %}
{% load form_helpers %}
{% load helpers %}
{% load render_table from django_tables2 %}
{% load i18n %}
@ -61,6 +62,7 @@ Context:
{% for field in form.hidden_fields %}
{{ field }}
{% endfor %}
{% render_field form.background_job %}
<div class="text-end">
<a href="{{ return_url }}" class="btn btn-outline-secondary">{% trans "Cancel" %}</a>
<button type="submit" name="_confirm" class="btn btn-danger">{% trans "Delete" %} {{ table.rows|length }} {{ model|meta:"verbose_name_plural" }}</button>

View File

@ -89,6 +89,8 @@ Context:
</div>
{% endif %}
{% render_field form.background_job %}
{% else %}
{# Render all fields #}

View File

@ -1,6 +1,10 @@
from dataclasses import dataclass
from typing import List
from django.contrib import messages
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _
from netbox.jobs import AsyncViewJob
from utilities.request import copy_safe_request
@ -38,9 +42,19 @@ def process_request_as_job(view, request, name=None):
request_copy._background = True
# Enqueue a job to perform the work in the background
return AsyncViewJob.enqueue(
job = AsyncViewJob.enqueue(
name=name,
user=request.user,
view_cls=view,
request=request_copy,
)
# Record a message on the original request indicating deferral to a background job
msg = _('Created background job {id}: <a href="{url}">{name}</a>').format(
id=job.pk,
url=job.get_absolute_url(),
name=job.name
)
messages.info(request, mark_safe(msg))
return job