Closes #36: Add changelog_enabled toggle to CustomObjectType#574
Closes #36: Add changelog_enabled toggle to CustomObjectType#574bctiemann wants to merge 13 commits into
Conversation
e744d0f to
98426bc
Compare
|
@claude /review and limit response to areas of potential concern. |
|
Claude finished @bctiemann's task in 2m 56s —— View job Review: Areas of Potential Concern
1. Migration is missing
|
|
Addressed items 2, 3, and 4. The migration is fine as-is ( |
arthanson
left a comment
There was a problem hiding this comment.
This is really ugly for branching, I think we might need to say this can only be set on creation and never modified (not sure if that breaks the intent) but if you toggle it mid-way and have items already in a branch I can see all sorts of potential bad side-effects.
Also please add a whole set of tests with branching for this - especially with custom objects having fields linking to and being linked from COT that have object-change disabled and run through the sync/merge/revet lifecycle - they can be added as extra test cases to the co-branching tests. Also make sure polymorphic fields are included as well as those are special case. so object, multi-object and those as polymorphic being in a regular CO linking to a non-object-change CO and the same ones in a non-object-change CO linking to a regular CO.
|
The "Changelog enabled" setting now cannot be changed after creation. guarded in the form and serializer. Also a set of unit tests have been added as recommended. However, note that these tests do not run in CI, so they will not proactively defend against regressions. I've opened #583 to add that pipeline to the CI matrix; it should run in parallel with the existing ones and not take any additional time. |
arthanson
left a comment
There was a problem hiding this comment.
- create a COT with changelog enabled = False
- create a CO of that type
- delete that CO
- crash
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "/Users/ahanson/dev/work/netbox/venv/lib/python3.14/site-packages/django/core/handlers/exception.py", line 55, in inner
response = get_response(request)
File "/Users/ahanson/dev/work/netbox/venv/lib/python3.14/site-packages/django/core/handlers/base.py", line 198, in _get_response
response = wrapped_callback(request, *callback_args, **callback_kwargs)
File "/Users/ahanson/dev/work/netbox/venv/lib/python3.14/site-packages/django/views/generic/base.py", line 106, in view
return self.dispatch(request, *args, **kwargs)
~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/ahanson/dev/work/netbox/netbox/netbox/views/generic/base.py", line 26, in dispatch
return super().dispatch(request, *args, **kwargs)
~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/ahanson/dev/work/netbox/netbox/utilities/views.py", line 152, in dispatch
return super().dispatch(request, *args, **kwargs)
~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/ahanson/dev/work/netbox/netbox/utilities/views.py", line 51, in dispatch
return super().dispatch(request, *args, **kwargs)
~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/ahanson/dev/work/netbox/venv/lib/python3.14/site-packages/django/views/generic/base.py", line 145, in dispatch
return handler(request, *args, **kwargs)
File "/Users/ahanson/dev/work/netbox/netbox/netbox/views/generic/object_views.py", line 482, in post
obj.delete()
~~~~~~~~~~^^
File "/Users/ahanson/dev/work/netbox-custom-objects/netbox_custom_objects/models.py", line 917, in delete
return super().delete(*args, **kwargs)
~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^
File "/Users/ahanson/dev/work/netbox/netbox/netbox/models/deletion.py", line 85, in delete
return collector.delete()
~~~~~~~~~~~~~~~~^^
File "/Users/ahanson/dev/work/netbox/venv/lib/python3.14/site-packages/django/db/models/deletion.py", line 462, in delete
signals.pre_delete.send(
~~~~~~~~~~~~~~~~~~~~~~~^
sender=model,
^^^^^^^^^^^^^
...<2 lines>...
origin=self.origin,
^^^^^^^^^^^^^^^^^^^
)
^
File "/Users/ahanson/dev/work/netbox/venv/lib/python3.14/site-packages/django/dispatch/dispatcher.py", line 209, in send
response = receiver(signal=self, sender=sender, **named)
File "/Users/ahanson/dev/work/netbox/netbox/core/signals.py", line 201, in handle_deleted_object
objectchange.user = request.user
^^^^^^^^^^^^^^^^^
AttributeError: 'NoneType' object has no attribute 'user' and no __dict__ for setting new attributes
|
Should be OK now. The fix is in the |
arthanson
left a comment
There was a problem hiding this comment.
still having issues:
- be in main
- create a COT cot1 (turn off ObjectChange), add field text f1
- create a CO of that type 'aaa'
- create a branch b1 and activate it
- create another CO of the cot1 type 'bbb'
- go to the list of cot1
- switch to main
what you will see is just one item in the list 'aaa'
since ObjectChange is off all items should be created in main branch so you should see two items in the list (both 'aaa' and 'bbb') in main and branch b1.
Adds a BooleanField 'changelog_enabled' (default True) to CustomObjectType. When disabled, the generated model's to_objectchange() returns None, causing NetBox's change-logging signal handler to skip writing ObjectChange rows — exactly what users need for high-frequency, low-audit-importance updates (e.g. nightly fleet scans across thousands of objects). Implementation: - Model field with migration 0015 - to_objectchange() override injected at get_model() time when disabled - Form: new 'Options' fieldset exposes the toggle in the UI - Serializer: field included in CustomObjectTypeSerializer - Table: field available as an optional column - Detail template: shows changelog status with checkmark Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…disabled
- Template: wraps the Changelog tab link in {% if object.custom_object_type.changelog_enabled %}
so the tab is absent from the navigation rather than visible-but-empty
- View: raises Http404 when the changelog URL is hit directly for a
changelog-disabled COT, preventing an empty page via direct URL
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Rename lambda params self->_self, action->_action to avoid shadowing get_model()'s 'self' (the CustomObjectType instance) - Add ChangelogEnabledTestCase: verifies to_objectchange() returns None directly, that ObjectChange rows are suppressed via the signal, and that the default (enabled) path still writes rows - Add ChangelogEnabledViewTestCase: verifies the changelog URL returns 404 for a disabled COT, and that the Changelog tab link is absent from the object detail template Migration is unchanged: --check confirms Django correctly omits verbose_name (matches the auto-generated value) and help_text (metadata-only, not stored in the schema). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… toggle test - Extend changelog_enabled help_text to note that disabling also prevents objects from participating in branching (branching requires change capture) - Add changelog_enabled to CustomObjectTypeFilterSet so operators can query ?changelog_enabled=false via the API or list-view filter - Add test_mid_lifecycle_toggle_disables_changelog: verifies that toggling an existing COT from True→False correctly suppresses ObjectChange rows for subsequent saves, exercising the cache_timestamp invalidation path Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
connect_signature_invalidation() guarded the branching import with ImportError, but Django raises RuntimeError (not ImportError) when a package is importable but absent from INSTALLED_APPS. Use apps.is_installed() instead, which is the correct check. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…branching tests - changelog_enabled is now locked after COT creation: - Form disables the field on edit with a 'cannot be changed' note - API serializer rejects updates that change the value - Model help_text updated to state the permanence This prevents mid-lifecycle branching issues (objects already in a branch, partial changelog entries, corrupt branch state on toggle). - Add ChangelogEnabledImmutabilityTestCase: verifies the form and serializer both enforce the lock. - Add ChangelogEnabledBranchingTestCase: verifies the key cross-COT scenarios Arthur identified — objects of a non-changelog COT can't be captured in a branch, and object/multiobject/polymorphic field references between changelog and non-changelog COTs don't break the branch sync/merge lifecycle. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…LLED_APPS" This reverts commit c9867db.
Two bugs in ChangelogEnabledBranchingTestCase caught by running with netbox-branching enabled: - merge_strategy='rebase' is not a valid strategy; use 'iterative' - branch.merge() takes user= and commit= kwargs, not a request object Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
handle_deleted_object in core/signals.py unconditionally calls objectchange.user = request.user with no None guard, unlike the save handler which checks 'if objectchange and objectchange.has_changes'. Returning None from to_objectchange() for delete actions therefore crashes on CO deletion. Fix: allow deletes through to the normal ChangeLoggingMixin path so the delete signal has a valid ObjectChange to work with. Only create and update actions suppress changelog recording. Deletes are rare and worth preserving for audit purposes; the high-frequency use case this flag targets is create/update, not deletion. Update test to assert None only for create/update and non-None for delete. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Arthur's scenario: objects created inside a branch context for a changelog-disabled COT were landing in the branch's PostgreSQL schema instead of main, so they disappeared when switching back to main. Root cause: netbox-branching routes writes to a branch-specific PostgreSQL schema for all ChangeLoggingMixin models. Our supports_branching_resolver in branching.py already handles M2M through models; extend it to return False for generated COT models whose CustomObjectType has changelog_enabled=False. The DB router then routes their reads/writes to the default (main) DB regardless of the active branch. - branching.py: supports_branching_resolver returns False when the generated model's custom_object_type.changelog_enabled is False - models.py: update help_text to say 'exempts from branch isolation' rather than the vague 'prevents participating in branching' - test_branching.py: add test_nolog_cot_objects_visible_in_main_when_created_in_branch covering Arthur's exact scenario (create in main, create in branch, verify both visible in main after deactivating branch) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
0137c6b to
40eda8e
Compare
During the rebase onto feature, git checkout --theirs on the live.py conflict reverted to a pre-fix version. feature already has the correct hoisted module-level 'from django.apps import apps as django_apps' from PR #573; restore that version. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Fixed. Thanks for doing the local testing on this, my custom-objects+branching setup is a bit messed up and I can't be sure I'm covering the right territory. |
Closes: #36
Summary
Adds a
changelog_enabledboolean field (defaultTrue) toCustomObjectType. When disabled, changes to objects of that type are silently skipped by NetBox's change-logging signal — noObjectChangerows are written, and the Changelog tab is hidden from the custom object detail page.How it works
The implementation is confined to two points:
Model field —
CustomObjectType.changelog_enabled(migration 0015, defaultTrue).Generated model override —
get_model()injects ato_objectchange()method that returnsNonewhen the flag is off. NetBox'shandle_changed_objectsignal guards onobjectchange and objectchange.has_changesbefore saving, soNoneis the documented way to suppress a change record — no signal disconnection, no monkey-patching of the signal itself.Surface area
changelog_enabledtoggleCustomObjectTypeSerializerchangelog_enabled=False; direct URL access to the changelog view returns 404Notes
changelog_enabled=False, objects of that type also cannot participate in branching (branching requires change capture). This is consistent with the issue's statement that "this user would be fine with these changes not being branchable."🤖 Generated with Claude Code