From aaaf346e5ff8ef7fed5c627399d460575aac53f5 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 17 Jul 2025 15:40:50 -0400 Subject: [PATCH 01/24] Add message field to ObjectChange model --- netbox/core/api/serializers_/change_logging.py | 3 ++- netbox/core/filtersets.py | 3 ++- .../core/migrations/0017_objectchange_message.py | 16 ++++++++++++++++ netbox/core/models/change_logging.py | 5 +++++ netbox/core/tables/change_logging.py | 8 +++++++- netbox/templates/core/objectchange.html | 8 +++++++- 6 files changed, 39 insertions(+), 4 deletions(-) create mode 100644 netbox/core/migrations/0017_objectchange_message.py diff --git a/netbox/core/api/serializers_/change_logging.py b/netbox/core/api/serializers_/change_logging.py index e8af31ae873..575a849d5b4 100644 --- a/netbox/core/api/serializers_/change_logging.py +++ b/netbox/core/api/serializers_/change_logging.py @@ -44,7 +44,8 @@ class Meta: model = ObjectChange fields = [ 'id', 'url', 'display_url', 'display', 'time', 'user', 'user_name', 'request_id', 'action', - 'changed_object_type', 'changed_object_id', 'changed_object', 'prechange_data', 'postchange_data', + 'changed_object_type', 'changed_object_id', 'changed_object', 'message', 'prechange_data', + 'postchange_data', ] @extend_schema_field(serializers.JSONField(allow_null=True)) diff --git a/netbox/core/filtersets.py b/netbox/core/filtersets.py index c64bb03ff57..9f90752d702 100644 --- a/netbox/core/filtersets.py +++ b/netbox/core/filtersets.py @@ -186,7 +186,8 @@ def search(self, queryset, name, value): return queryset return queryset.filter( Q(user_name__icontains=value) | - Q(object_repr__icontains=value) + Q(object_repr__icontains=value) | + Q(message__icontains=value) ) diff --git a/netbox/core/migrations/0017_objectchange_message.py b/netbox/core/migrations/0017_objectchange_message.py new file mode 100644 index 00000000000..6abfbd388bc --- /dev/null +++ b/netbox/core/migrations/0017_objectchange_message.py @@ -0,0 +1,16 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0016_job_log_entries'), + ] + + operations = [ + migrations.AddField( + model_name='objectchange', + name='message', + field=models.CharField(blank=True, editable=False), + ), + ] diff --git a/netbox/core/models/change_logging.py b/netbox/core/models/change_logging.py index 1d1bbc07c83..d00e7a909b5 100644 --- a/netbox/core/models/change_logging.py +++ b/netbox/core/models/change_logging.py @@ -82,6 +82,11 @@ class ObjectChange(models.Model): max_length=200, editable=False ) + message = models.CharField( + verbose_name=_('message'), + editable=False, + blank=True + ) prechange_data = models.JSONField( verbose_name=_('pre-change data'), editable=False, diff --git a/netbox/core/tables/change_logging.py b/netbox/core/tables/change_logging.py index aced0e8a637..b35b711bb9c 100644 --- a/netbox/core/tables/change_logging.py +++ b/netbox/core/tables/change_logging.py @@ -41,6 +41,9 @@ class ObjectChangeTable(NetBoxTable): template_code=OBJECTCHANGE_REQUEST_ID, verbose_name=_('Request ID') ) + message = tables.Column( + verbose_name=_('Message'), + ) actions = columns.ActionsColumn( actions=() ) @@ -49,5 +52,8 @@ class Meta(NetBoxTable.Meta): model = ObjectChange fields = ( 'pk', 'time', 'user_name', 'full_name', 'action', 'changed_object_type', 'object_repr', 'request_id', - 'actions', + 'message', 'actions', + ) + default_columns = ( + 'pk', 'time', 'user_name', 'action', 'changed_object_type', 'object_repr', 'message', 'actions', ) diff --git a/netbox/templates/core/objectchange.html b/netbox/templates/core/objectchange.html index ae32e44dbe2..e4c7d490062 100644 --- a/netbox/templates/core/objectchange.html +++ b/netbox/templates/core/objectchange.html @@ -64,10 +64,16 @@

{% trans "Change" %}

{% endif %} + + {% trans "Message" %} + + {{ object.message|placeholder }} + + {% trans "Request ID" %} - {{ object.request_id }} + {{ object.request_id }} From 5e5c46f77c4115c5b1a85ec275ae083bdbca284e Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 17 Jul 2025 16:20:35 -0400 Subject: [PATCH 02/24] Set max length on changelog message --- netbox/core/migrations/0017_objectchange_message.py | 2 +- netbox/core/models/change_logging.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/netbox/core/migrations/0017_objectchange_message.py b/netbox/core/migrations/0017_objectchange_message.py index 6abfbd388bc..c669513a09c 100644 --- a/netbox/core/migrations/0017_objectchange_message.py +++ b/netbox/core/migrations/0017_objectchange_message.py @@ -11,6 +11,6 @@ class Migration(migrations.Migration): migrations.AddField( model_name='objectchange', name='message', - field=models.CharField(blank=True, editable=False), + field=models.CharField(blank=True, editable=False, max_length=200), ), ] diff --git a/netbox/core/models/change_logging.py b/netbox/core/models/change_logging.py index d00e7a909b5..819b1b2b33a 100644 --- a/netbox/core/models/change_logging.py +++ b/netbox/core/models/change_logging.py @@ -84,6 +84,7 @@ class ObjectChange(models.Model): ) message = models.CharField( verbose_name=_('message'), + max_length=200, editable=False, blank=True ) From 8d5436876ed2ef7c5321864b689d7edd1fc87de5 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 17 Jul 2025 16:37:42 -0400 Subject: [PATCH 03/24] Enable changelog messages for single object operations --- netbox/netbox/forms/base.py | 4 ++-- netbox/netbox/forms/mixins.py | 15 +++++++++++++++ netbox/netbox/models/features.py | 5 ++++- netbox/netbox/views/generic/object_views.py | 7 ++++--- netbox/templates/htmx/form.html | 4 ++++ netbox/utilities/forms/forms.py | 11 +++++++++++ 6 files changed, 40 insertions(+), 6 deletions(-) diff --git a/netbox/netbox/forms/base.py b/netbox/netbox/forms/base.py index 57cfd180100..c09e8f7fd90 100644 --- a/netbox/netbox/forms/base.py +++ b/netbox/netbox/forms/base.py @@ -11,7 +11,7 @@ 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 +from .mixins import ChangeLoggingMixin, CustomFieldsMixin, SavedFiltersMixin, TagsMixin __all__ = ( 'NetBoxModelForm', @@ -21,7 +21,7 @@ ) -class NetBoxModelForm(CheckLastUpdatedMixin, CustomFieldsMixin, TagsMixin, forms.ModelForm): +class NetBoxModelForm(ChangeLoggingMixin, CheckLastUpdatedMixin, CustomFieldsMixin, TagsMixin, forms.ModelForm): """ Base form for creating & editing NetBox models. Extends Django's ModelForm to add support for custom fields. diff --git a/netbox/netbox/forms/mixins.py b/netbox/netbox/forms/mixins.py index c569343ee64..b064871415f 100644 --- a/netbox/netbox/forms/mixins.py +++ b/netbox/netbox/forms/mixins.py @@ -7,12 +7,27 @@ from utilities.forms.fields import DynamicModelMultipleChoiceField __all__ = ( + 'ChangeLoggingMixin', 'CustomFieldsMixin', 'SavedFiltersMixin', 'TagsMixin', ) +class ChangeLoggingMixin(forms.Form): + changelog_message = forms.CharField( + required=False, + 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: """ Extend a Form to include custom field support. diff --git a/netbox/netbox/models/features.py b/netbox/netbox/models/features.py index 79145ce7020..023259cc8cc 100644 --- a/netbox/netbox/models/features.py +++ b/netbox/netbox/models/features.py @@ -63,6 +63,8 @@ class ChangeLoggingMixin(DeleteMixin, models.Model): null=True ) + _changelog_message = None + class Meta: abstract = True @@ -103,7 +105,8 @@ def to_objectchange(self, action): objectchange = ObjectChange( changed_object=self, object_repr=str(self)[:200], - action=action + action=action, + message=self._changelog_message or '', ) if hasattr(self, '_prechange_snapshot'): objectchange.prechange_data = self._prechange_snapshot diff --git a/netbox/netbox/views/generic/object_views.py b/netbox/netbox/views/generic/object_views.py index 5bc79d9629e..a8c50c1c42b 100644 --- a/netbox/netbox/views/generic/object_views.py +++ b/netbox/netbox/views/generic/object_views.py @@ -19,7 +19,7 @@ ) from utilities.error_handlers import handle_protectederror from utilities.exceptions import AbortRequest, PermissionsViolation -from utilities.forms import ConfirmationForm, restrict_form_fields +from utilities.forms import DeleteForm, restrict_form_fields from utilities.htmx import htmx_partial from utilities.permissions import get_permission_for_model from utilities.querydict import normalize_querydict, prepare_cloned_fields @@ -422,7 +422,7 @@ def get(self, request, *args, **kwargs): request: The current request """ obj = self.get_object(**kwargs) - form = ConfirmationForm(initial=request.GET) + form = DeleteForm(initial=request.GET) try: dependent_objects = self._get_dependent_objects(obj) @@ -461,7 +461,7 @@ def post(self, request, *args, **kwargs): """ logger = logging.getLogger('netbox.views.ObjectDeleteView') obj = self.get_object(**kwargs) - form = ConfirmationForm(request.POST) + form = DeleteForm(request.POST) # Take a snapshot of change-logged models if hasattr(obj, 'snapshot'): @@ -469,6 +469,7 @@ def post(self, request, *args, **kwargs): if form.is_valid(): logger.debug("Form validation was successful") + obj._changelog_message = form.cleaned_data.pop('changelog_message', '') try: obj.delete() diff --git a/netbox/templates/htmx/form.html b/netbox/templates/htmx/form.html index 530a1805463..1108a6113bd 100644 --- a/netbox/templates/htmx/form.html +++ b/netbox/templates/htmx/form.html @@ -28,6 +28,10 @@

{% trans "Custom Fields" %}

{% endif %} + {% if form.changelog_message %} + {% render_field form.changelog_message %} + {% endif %} + {% else %} {# Render all fields in a single group #} diff --git a/netbox/utilities/forms/forms.py b/netbox/utilities/forms/forms.py index 2192c5a9974..122107728d1 100644 --- a/netbox/utilities/forms/forms.py +++ b/netbox/utilities/forms/forms.py @@ -10,6 +10,7 @@ 'BulkRenameForm', 'ConfirmationForm', 'CSVModelForm', + 'DeleteForm', 'FilterForm', 'TableConfigForm', ) @@ -30,6 +31,16 @@ class ConfirmationForm(forms.Form): ) +class DeleteForm(ConfirmationForm): + """ + Confirm the deletion of an object, optionally providing a changelog message. + """ + changelog_message = forms.CharField( + required=False, + max_length=200 + ) + + class BulkEditForm(BackgroundJobMixin, forms.Form): """ Provides bulk edit support for objects. From 1b11895c90d726124b603ba1fdf5ffeb1f47eed0 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 17 Jul 2025 16:38:51 -0400 Subject: [PATCH 04/24] Fix tests --- netbox/core/tests/test_filtersets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/core/tests/test_filtersets.py b/netbox/core/tests/test_filtersets.py index b7dfd516e24..4b2cff84d40 100644 --- a/netbox/core/tests/test_filtersets.py +++ b/netbox/core/tests/test_filtersets.py @@ -150,7 +150,7 @@ def test_hash(self): class ObjectChangeTestCase(TestCase, BaseFilterSetTests): queryset = ObjectChange.objects.all() filterset = ObjectChangeFilterSet - ignore_fields = ('prechange_data', 'postchange_data') + ignore_fields = ('message', 'prechange_data', 'postchange_data') @classmethod def setUpTestData(cls): From 0703fe78526bab5ec144743d6f713d7adc6a6a6b Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 18 Jul 2025 10:31:42 -0400 Subject: [PATCH 05/24] Add changelog message support for bulk edit & bulk delete --- netbox/netbox/forms/base.py | 2 +- netbox/netbox/forms/mixins.py | 7 ------- netbox/netbox/views/generic/bulk_views.py | 12 +++++++++++- netbox/netbox/views/generic/object_views.py | 16 ++++++++++------ netbox/templates/generic/bulk_delete.html | 1 + netbox/templates/generic/bulk_edit.html | 1 + 6 files changed, 24 insertions(+), 15 deletions(-) diff --git a/netbox/netbox/forms/base.py b/netbox/netbox/forms/base.py index c09e8f7fd90..4b8f7027df7 100644 --- a/netbox/netbox/forms/base.py +++ b/netbox/netbox/forms/base.py @@ -100,7 +100,7 @@ def _get_form_field(self, customfield): 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 fields and adding/removing tags. diff --git a/netbox/netbox/forms/mixins.py b/netbox/netbox/forms/mixins.py index b064871415f..4c9e46c0e44 100644 --- a/netbox/netbox/forms/mixins.py +++ b/netbox/netbox/forms/mixins.py @@ -20,13 +20,6 @@ class ChangeLoggingMixin(forms.Form): 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: """ diff --git a/netbox/netbox/views/generic/bulk_views.py b/netbox/netbox/views/generic/bulk_views.py index 68632688158..246f41f3b09 100644 --- a/netbox/netbox/views/generic/bulk_views.py +++ b/netbox/netbox/views/generic/bulk_views.py @@ -21,6 +21,7 @@ from core.signals import clear_events from extras.choices import CustomFieldUIEditableChoices from extras.models import CustomField, ExportTemplate +from netbox.forms.mixins import ChangeLoggingMixin from netbox.object_actions import AddObject, BulkDelete, BulkEdit, BulkExport, BulkImport, BulkRename from utilities.error_handlers import handle_protectederror from utilities.exceptions import AbortRequest, AbortTransaction, PermissionsViolation @@ -622,6 +623,9 @@ def _update_objects(self, form, request): if hasattr(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. for name, model_field in model_fields.items(): # Handle nullification @@ -892,7 +896,7 @@ def get_form(self): """ 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) return BulkDeleteForm @@ -939,9 +943,15 @@ def post(self, request, **kwargs): try: with transaction.atomic(using=router.db_for_write(model)): for obj in queryset: + # Take a snapshot of change-logged models if hasattr(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() except (ProtectedError, RestrictedError) as e: diff --git a/netbox/netbox/views/generic/object_views.py b/netbox/netbox/views/generic/object_views.py index a8c50c1c42b..657f95f1fd2 100644 --- a/netbox/netbox/views/generic/object_views.py +++ b/netbox/netbox/views/generic/object_views.py @@ -288,6 +288,9 @@ def post(self, request, *args, **kwargs): if form.is_valid(): logger.debug("Form validation was successful") + # Record changelog message (if any) + obj._changelog_message = form.cleaned_data.pop('changelog_message', '') + try: with transaction.atomic(using=router.db_for_write(model)): object_created = form.instance.pk is None @@ -463,22 +466,23 @@ def post(self, request, *args, **kwargs): obj = self.get_object(**kwargs) form = DeleteForm(request.POST) - # Take a snapshot of change-logged models - if hasattr(obj, 'snapshot'): - obj.snapshot() - if form.is_valid(): 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', '') + # Delete the object try: obj.delete() - except (ProtectedError, RestrictedError) as e: logger.info(f"Caught {type(e)} while attempting to delete objects") handle_protectederror([obj], request, e) return redirect(obj.get_absolute_url()) - except AbortRequest as e: logger.debug(e.message) messages.error(request, mark_safe(e.message)) diff --git a/netbox/templates/generic/bulk_delete.html b/netbox/templates/generic/bulk_delete.html index 594efff63d7..02180348552 100644 --- a/netbox/templates/generic/bulk_delete.html +++ b/netbox/templates/generic/bulk_delete.html @@ -67,6 +67,7 @@

{% trans "Confirm Bulk Deletion" %}

{# Meta fields #}
+ {% render_field form.changelog_message %} {% render_field form.background_job %}
diff --git a/netbox/templates/generic/bulk_edit.html b/netbox/templates/generic/bulk_edit.html index 6aace8786fe..f41b88ef58b 100644 --- a/netbox/templates/generic/bulk_edit.html +++ b/netbox/templates/generic/bulk_edit.html @@ -104,6 +104,7 @@

{% trans "Comments" %}

{# Meta fields #}
+ {% render_field form.changelog_message %} {% render_field form.background_job %}
From 2044802586d700ed02e148a4976743c9dfcbde02 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 18 Jul 2025 11:19:39 -0400 Subject: [PATCH 06/24] Cosmetic improvements to form fields --- netbox/templates/generic/bulk_delete.html | 2 +- netbox/templates/generic/bulk_edit.html | 2 +- netbox/templates/generic/object_delete.html | 2 +- netbox/templates/htmx/delete_form.html | 19 ++++++++++++++++--- netbox/templates/htmx/form.html | 5 ++++- netbox/templates/inc/htmx_modal.html | 2 +- 6 files changed, 24 insertions(+), 8 deletions(-) diff --git a/netbox/templates/generic/bulk_delete.html b/netbox/templates/generic/bulk_delete.html index 02180348552..37f9cef5c02 100644 --- a/netbox/templates/generic/bulk_delete.html +++ b/netbox/templates/generic/bulk_delete.html @@ -66,7 +66,7 @@

{% trans "Confirm Bulk Deletion" %}

{% endfor %} {# Meta fields #} -
+
{% render_field form.changelog_message %} {% render_field form.background_job %}
diff --git a/netbox/templates/generic/bulk_edit.html b/netbox/templates/generic/bulk_edit.html index f41b88ef58b..687739e5020 100644 --- a/netbox/templates/generic/bulk_edit.html +++ b/netbox/templates/generic/bulk_edit.html @@ -103,7 +103,7 @@

{% trans "Comments" %}

{% endif %} {# Meta fields #} -
+
{% render_field form.changelog_message %} {% render_field form.background_job %}
diff --git a/netbox/templates/generic/object_delete.html b/netbox/templates/generic/object_delete.html index bde8830d9ad..71d8d39847b 100644 --- a/netbox/templates/generic/object_delete.html +++ b/netbox/templates/generic/object_delete.html @@ -20,7 +20,7 @@ {% endblock %} {% block content %} -