Add changelog message support for bulk edit & bulk delete

This commit is contained in:
Jeremy Stretch 2025-07-18 10:31:42 -04:00
parent 1b11895c90
commit 0703fe7852
6 changed files with 24 additions and 15 deletions

View File

@ -100,7 +100,7 @@ class NetBoxModelImportForm(CSVModelForm, NetBoxModelForm):
return customfield.to_form_field(for_csv_import=True) return customfield.to_form_field(for_csv_import=True)
class NetBoxModelBulkEditForm(CustomFieldsMixin, BulkEditForm): class NetBoxModelBulkEditForm(ChangeLoggingMixin, CustomFieldsMixin, BulkEditForm):
""" """
Base form for modifying multiple NetBox objects (of the same type) in bulk via the UI. Adds support for custom 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. fields and adding/removing tags.

View File

@ -20,13 +20,6 @@ class ChangeLoggingMixin(forms.Form):
max_length=200 max_length=200
) )
def clean(self):
# Attach the changelog message (if any) to the instance
self.instance._changelog_message = self.cleaned_data.pop('changelog_message', None)
return self.cleaned_data
class CustomFieldsMixin: class CustomFieldsMixin:
""" """

View File

@ -21,6 +21,7 @@ from core.models import ObjectType
from core.signals import clear_events from core.signals import clear_events
from extras.choices import CustomFieldUIEditableChoices from extras.choices import CustomFieldUIEditableChoices
from extras.models import CustomField, ExportTemplate from extras.models import CustomField, ExportTemplate
from netbox.forms.mixins import ChangeLoggingMixin
from netbox.object_actions import AddObject, BulkDelete, BulkEdit, BulkExport, BulkImport, BulkRename from netbox.object_actions import AddObject, BulkDelete, BulkEdit, BulkExport, BulkImport, BulkRename
from utilities.error_handlers import handle_protectederror from utilities.error_handlers import handle_protectederror
from utilities.exceptions import AbortRequest, AbortTransaction, PermissionsViolation from utilities.exceptions import AbortRequest, AbortTransaction, PermissionsViolation
@ -622,6 +623,9 @@ class BulkEditView(GetReturnURLMixin, BaseMultiObjectView):
if hasattr(obj, 'snapshot'): if hasattr(obj, 'snapshot'):
obj.snapshot() obj.snapshot()
# Attach the changelog message (if any) to the object
obj._changelog_message = form.cleaned_data.get('changelog_message')
# Update standard fields. If a field is listed in _nullify, delete its value. # Update standard fields. If a field is listed in _nullify, delete its value.
for name, model_field in model_fields.items(): for name, model_field in model_fields.items():
# Handle nullification # Handle nullification
@ -892,7 +896,7 @@ class BulkDeleteView(GetReturnURLMixin, BaseMultiObjectView):
""" """
Provide a standard bulk delete form if none has been specified for the view Provide a standard bulk delete form if none has been specified for the view
""" """
class BulkDeleteForm(BackgroundJobMixin, ConfirmationForm): class BulkDeleteForm(BackgroundJobMixin, ChangeLoggingMixin, ConfirmationForm):
pk = ModelMultipleChoiceField(queryset=self.queryset, widget=MultipleHiddenInput) pk = ModelMultipleChoiceField(queryset=self.queryset, widget=MultipleHiddenInput)
return BulkDeleteForm return BulkDeleteForm
@ -939,9 +943,15 @@ class BulkDeleteView(GetReturnURLMixin, BaseMultiObjectView):
try: try:
with transaction.atomic(using=router.db_for_write(model)): with transaction.atomic(using=router.db_for_write(model)):
for obj in queryset: for obj in queryset:
# Take a snapshot of change-logged models # Take a snapshot of change-logged models
if hasattr(obj, 'snapshot'): if hasattr(obj, 'snapshot'):
obj.snapshot() obj.snapshot()
# Attach the changelog message (if any) to the object
obj._changelog_message = form.cleaned_data.get('changelog_message')
# Delete the object
obj.delete() obj.delete()
except (ProtectedError, RestrictedError) as e: except (ProtectedError, RestrictedError) as e:

View File

@ -288,6 +288,9 @@ class ObjectEditView(GetReturnURLMixin, BaseObjectView):
if form.is_valid(): if form.is_valid():
logger.debug("Form validation was successful") logger.debug("Form validation was successful")
# Record changelog message (if any)
obj._changelog_message = form.cleaned_data.pop('changelog_message', '')
try: try:
with transaction.atomic(using=router.db_for_write(model)): with transaction.atomic(using=router.db_for_write(model)):
object_created = form.instance.pk is None object_created = form.instance.pk is None
@ -463,22 +466,23 @@ class ObjectDeleteView(GetReturnURLMixin, BaseObjectView):
obj = self.get_object(**kwargs) obj = self.get_object(**kwargs)
form = DeleteForm(request.POST) form = DeleteForm(request.POST)
# Take a snapshot of change-logged models
if hasattr(obj, 'snapshot'):
obj.snapshot()
if form.is_valid(): if form.is_valid():
logger.debug("Form validation was successful") logger.debug("Form validation was successful")
# Take a snapshot of change-logged models
if hasattr(obj, 'snapshot'):
obj.snapshot()
# Record changelog message (if any)
obj._changelog_message = form.cleaned_data.pop('changelog_message', '') obj._changelog_message = form.cleaned_data.pop('changelog_message', '')
# Delete the object
try: try:
obj.delete() obj.delete()
except (ProtectedError, RestrictedError) as e: except (ProtectedError, RestrictedError) as e:
logger.info(f"Caught {type(e)} while attempting to delete objects") logger.info(f"Caught {type(e)} while attempting to delete objects")
handle_protectederror([obj], request, e) handle_protectederror([obj], request, e)
return redirect(obj.get_absolute_url()) return redirect(obj.get_absolute_url())
except AbortRequest as e: except AbortRequest as e:
logger.debug(e.message) logger.debug(e.message)
messages.error(request, mark_safe(e.message)) messages.error(request, mark_safe(e.message))

View File

@ -67,6 +67,7 @@ Context:
{# Meta fields #} {# Meta fields #}
<div class="bg-primary-subtle border border-primary rounded-1 pt-3 mb-3"> <div class="bg-primary-subtle border border-primary rounded-1 pt-3 mb-3">
{% render_field form.changelog_message %}
{% render_field form.background_job %} {% render_field form.background_job %}
</div> </div>

View File

@ -104,6 +104,7 @@ Context:
{# Meta fields #} {# Meta fields #}
<div class="bg-primary-subtle border border-primary rounded-1 pt-3 mb-3"> <div class="bg-primary-subtle border border-primary rounded-1 pt-3 mb-3">
{% render_field form.changelog_message %}
{% render_field form.background_job %} {% render_field form.background_job %}
</div> </div>