All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
- Bumped supported version floors to track upstream
netbox-custom-objectsv0.5.1 (release notes):PluginConfig.min_version:4.5.0→4.5.2(mirrors upstream's own NetBox floor bump in #511 — keeps both gates consistent so a NetBox 4.5.0/4.5.1 host cannot end up with our plugin loading whilenetbox-custom-objectsitself refuses to start).netbox-custom-objectsruntime floor:≥ 0.5.1(was≥ 0.5.0). TheImproperlyConfiguredmessage inPluginConfig.ready()now points users atpip install -U 'netbox-custom-objects>=0.5.1'. The behavioural probe (CustomObjectTypeField._meta.get_field("is_polymorphic")) is unchanged — it still keys off the 0.5.0 schema sentinel, since no field added in 0.5.1 is a reliable runtime marker — but the user-facing recommendation advances to 0.5.1, which fixes the upstream Delete bug previously called out under Known Issues as well as several cross-COT FK and M2M-deletion regressions.
- No code-logic changes were required to follow v0.5.1.
combined.py::_iter_linked_fieldsandtyped.py::_build_q_for_fieldalready filter byinstance.pk(int) rather than by model instance, so upstream's fix for issue #508 (CustomObjectLink.left_page()rewrite fromfilter(**{field.name: target_obj})tofilter(**{f"{field.name}_id": target_obj.pk})) does not affect us. The M2Mpath_infosrepair from #483 is applied insideCustomObjectType.get_model(), which we call per request, so we inherit the fix for free.
- Active CSS class missing on Custom Object Journal/Changelog tabs
(#15) —
on Custom Object detail pages, clicking the Journal or Changelog tab
loaded the right page but never highlighted the clicked tab. Root cause
was the 2.1.0 template-override refactor (commit
37ccf6b), which replaced upstream's hardcoded Journal/Changelog<li>blocks with a single{% model_view_tabs object %}call. UpstreamCustomObjectJournalView/CustomObjectChangeLogView(netbox_custom_objects/views.py:1321, 1393) inject the literal strings"journal"/"changelog"into the template context as the active-tab marker, whilemodel_view_tabs(utilities/templatetags/tabs.py:53) computesis_active = active_tab == tabwheretabis aViewTabobject registered for the model — the string-vs-object comparison is always False, so theactiveclass is never emitted. Verified still present onnetboxlabs-netbox-custom-objects0.5.0 and 0.5.1 under NetBox 4.6.1. Fix renders Journal and Changelog as hardcoded<li>blocks in the override template (matching upstream's pre-override markup, with the string equality check) and introduces a new{% plugin_extra_tabs %}template tag (netbox_custom_objects_tab/templatetags/custom_object_tab_tags.py) that mirrorsmodel_view_tabsbut skips thejournal/changelogactions — required because NetBox auto-registersObjectJournalView/ObjectChangeLogViewfor every ChangeLoggedModel subclass innetbox/models/features.py:737-742, so without the filter the registry-driven render would emit duplicate inert Journal/Changelog tabs. Tab order is now Primary → combined/typed (registry) → Journal → Changelog, which incidentally matches the natural ViewTab weight ordering (combined=2000, typed=2100, Journal=5000, Changelog=10000) and stays stable if upstream later switches their context totab=self.tab(at which point the hardcoded blocks and the custom tag can be retired).
- Polymorphic Object / MultiObject field support
(#12) —
netbox-custom-objectsv0.5.0 introducedis_polymorphic=Truefields whose targets live in therelated_object_typesM2M (and, for MultiObject, in a per-field through table keyed by(source_id, content_type_id, object_id)). The plugin's discovery logic previously filtered only on the single-FKrelated_object_type, so polymorphic links were invisible: tabs disappeared from referenced hosts (Device, Interface, Site, other CO Types) until the field was switched back to non-polymorphic. Both tab styles now mirror upstream'sCustomObjectLink.left_pagequery shape — a non-polymorphic queryset plus a polymorphic queryset, with per-field branching into four reverse lookups (FK column, GFK(content_type_id, object_id)pair, M2M, polymorphic through table). - Polymorphic-field "Add Type" toolbar on typed tabs — the toolbar
shortcut now works for polymorphic Object and MultiObject fields using
upstream's add-view sub-field prefill format:
?<name>__ct=<host_ct>&<name>__obj=<host_pk>for polymorphic Object, and?<name>__<host_app>__<host_model>=<host_pk>for polymorphic MultiObject (the upstream form synthesizes oneDynamicModelMultipleChoiceFieldper allowed target type; we fill only the one matching the host). The previous behaviour silently skipped the Add link for polymorphic fields.
- Minimum
netbox-custom-objectsis now 0.5.0. Enforced at startup:PluginConfig.ready()probes for theis_polymorphicmodel field and raisesImproperlyConfiguredwith a clear upgrade message if the installed upstream is older. The check is behaviour-based (looks for the field directly, not a version string) so it remains correct against forks and pre-release tags. The pre-0.5 compat shims (getattrguards, module feature probes) have been removed from bothcombined.pyandtyped.py.
- Upstream Delete bug on
netbox-custom-objects == 0.5.0(fixed in 0.5.1). Deleting a Custom Object via the NetBox UI on 0.5.0 can raiseValueError: Cannot query "...": Must be "Table<N>Model" instance.fromCustomObjectDeleteView._get_dependent_objects. The same crash also hitsCustomObjectBulkDeleteView(NetBox's genericBulkDeleteView.post()iterates and callsobj.delete()per row — same code path), so Bulk Delete is NOT a workaround (2.3.0 README claim corrected). Resolution: upgrade upstream — PR #501 (merged intomain2026-05-11) eliminates the entire bug class. As of the 2.4.0 release date, no0.5.1release tag exists yet; install frommain(pip install git+...@main) or pin to>=0.5.1once tagged. Adjacent fixes for related drift paths (#504, #505, #510) also ship inmain/0.5.1. - Polymorphic-MultiObject rows amplify the failure rate on 0.5.0. Each
polymorphic Object field adds a
GenericForeignKeydescriptor and each polymorphic MultiObject field adds a per-field through model; Django's collector traverses every related model during deletion, so polymorphic rows give the drift more chances to land. Plugin 2.4.0's discovery code walks these descriptors (the original goal of 2.4.0) and warms the cache enough that the upstream drift becomes deterministic rather than intermittent on 0.5.0. - Workarounds for installs that cannot upgrade yet:
manage.py shelldirect delete (single-process model cache, no drift — see README) orsystemctl restart netbox(clears the cache). No UI-side workaround exists for 0.5.0. - Root cause is upstream (
netbox-custom-objectsdynamic-model caching). This plugin does not override delete or model caching and cannot fix it from its own code; PR #501 fixes it inside upstream's delete view. - Cosmetic post-fix issue on patched builds. On builds containing
PR #501 (upstream
main/ forthcoming 0.5.1), the delete-success toast for some dynamic models renders as"Deleted <Type> <Type> None"— the patched view callsstr(obj)after the row's deletion, so the primary field returnsNone. Cosmetic only; the delete itself works. Worth a small upstream follow-up issue but not a blocker.
- Add button on Typed tabs (#9) —
each Typed tab now shows an "Add Type" button in the bottom toolbar
(alongside Bulk Edit and Bulk Delete) that opens the native
customobject_addview with the reverse-reference field pre-filled to the parent object's PK andreturn_urlset back to the tab. After saving, the user lands back on the same tab, with any active filters preserved. When a Custom Object Type has multiple fields referencing the same parent model (e.g.primary_deviceandbackup_deviceboth → Device), the button becomes a split-dropdown listing each field. The button is hidden for users withoutadd_customobjectpermission.
- Typed-tab URL registration: typed-tab views are now registered
synchronously inside
AppConfig.ready()instead of from arequest_startedsignal handler. The earlier deferral (commit5bf09c3, PR #4) silenced some startup warnings but broke typed-tab routing entirely — NetBox'sget_model_urls()snapshotsregistry['views']when each model'surls.pyis first imported, so any view added afterward has no URL pattern. Combined tabs were unaffected because they were already synchronous; typed tabs were unreachable on every deployment withtyped_modelsconfigured. TheOperationalError/ProgrammingErrorsafety net insideregister_typed_tabsstill covers themanage.py migrate/ fresh-DB case. - Typed-tab badge no longer over-counts rows that match the parent via
multiple fields.
_count_for_typepreviously summed per-field counts with no deduplication, so a Custom Object Type with several fields pointing to the same parent model (e.g.primary_device+backup_device+affected_devicesall →dcim.device) reported a badge number larger than the actual table row count whenever a row matched the parent via more than one field. Now uses the sameQ-OR-Q + .distinct()pattern as the table queryset, so the badge and the table always agree. Bonus: one SQL query per tab badge instead of N (one per Device-pointing field). Bug existed since the typed-tab feature was introduced in 2.0.0; only became visible with multi-FK or M2M field combinations. - Typed-tab Bulk Edit / Bulk Delete buttons are now permission-gated
against
netbox_custom_objects.change_customobject/delete_customobjectrespectively, matching the gating pattern the Add button uses. Previously these buttons rendered unconditionally on Typed tabs; clicks were rejected server-side by NetBox'scustomobject_bulk_*views but the unguarded UI render was confusing for non-superusers. Per-button guards (rather than gating the whole toolbar onchange AND delete) so a user with onlychangeperm sees Bulk Edit but not Bulk Delete, and vice versa. Surfaced by the 2.3.0 smoke test with non-admin test users.
- Upstream
netbox_custom_objectsbug surfaced by the new Add button: deleting a custom object immediately after creating it via the 2.3.0 Add button (Create → row dropdown → Delete in the typed tab list) raisesValueError: Cannot query "X": Must be "Table<N>Model" instance.fromCustomObjectDeleteView._get_dependent_objects(upstreamnetbox_custom_objects/views.py:977). The error fires only on the first delete GET in that flow; refreshing the list page before clicking Delete works around it, and Bulk Delete (different code path) is unaffected. Root cause is dynamic-model class identity drift across the Create → Delete request boundary in upstream code (the dynamic model class registry rebuilds during Create, but the immediately- following Delete request still holds a reference to the previous class in some scope). Tracked here as a documentation-only release note since the fix needs to land innetbox_custom_objects, not in this plugin. See README "Known Issues" section for user-facing workarounds.
- Widen supported NetBox range to 4.5.0 – 4.6.99 (
max_versionbumped from4.5.99to4.6.99). No code or template changes were required: every NetBox API the plugin depends on —ViewTab,register_model_view,htmx_partial,EnhancedPaginator,get_paginate_count,BaseTable,NetBoxModelFilterSetForm,SavedFiltersMixin,TagFilterField,CustomFieldTypeChoices,CustomFieldUIVisibleChoices, and theregistry['views']shape — is unchanged in NetBox 4.6 (verified against thev4.6.0upstream tag). The 4.6 deprecations ofregistry['models']and legacyactions = {...}view dicts do not affect this plugin. - On NetBox 4.6, the upstream
netbox_custom_objectsplugin ≥ 0.5.0 is recommended (itsmax_versioncovers 4.6.99). The CO detail-page template override remains necessary —customobject.htmlin upstream v0.5.0 still hardcodes its{% block tabs %}without{% model_view_tabs object %}.
- CO→CO tabs —
netbox_custom_objects.*is now a valid value for bothcombined_modelsandtyped_models. This enables tabs on Custom Object detail pages themselves: when Custom Object Type A has a field (FK or M2M) pointing to Custom Object Type B, navigating to a Type B instance shows a tab listing all Type A instances that reference it. A NetBox restart is required whenever a new Custom Object Type is added (same requirement as all typed tabs). template_override.py— prepends our templates directory to Django's filesystem loader atready()time so that ournetbox_custom_objects/customobject.htmloverride (which adds{% model_view_tabs object %}) is found before the original template.
- Tab views now accept
**kwargsin theirget()method, accommodating the extracustom_object_typeURL keyword argument present on Custom Object detail URLs. base_templatefor Custom Object model instances now correctly resolves tonetbox_custom_objects/customobject.htmlinstead of the nonexistent per-model template._inject_co_urls()appends the necessary URL patterns for CO tab views intonetbox_custom_objects.urlsat startup, enabling URL reversal for registered tabs (thenetbox_custom_objectsplugin uses a single generic view and never registers per-model URL patterns for dynamic models).
- TypeError on typed tab — removed
user=keyword argument fromCustomObjectTableinstantiation.django_tables2.Table.__init__does not accept this kwarg; it was redundant becausetable.configure(request)already applies per-user column preferences. Fixes crash on NetBox 4.5.4-Docker (netbox_custom_objects0.4.6).
- Plugin version is now defined only in
pyproject.tomland read at runtime viaimportlib.metadata.version(), eliminating the duplicate version string in__init__.py.
- Typed tabs (per-type) — each Custom Object Type gets its own tab with a full-featured list view: type-specific columns, filterset sidebar, bulk edit/delete, configure table, and HTMX pagination.
typed_modelsandtyped_weightconfig settings.- Third-party plugin model support for both tab modes.
- Renamed
modelsconfig tocombined_models;labeltocombined_label;weighttocombined_weight. - Refactored views from single
views.pytoviews/package (__init__.py,combined.py,typed.py). - Templates reorganized into
combined/andtyped/subdirectories.
- Handle missing database during startup —
register_typed_tabs()now catchesOperationalErrorandProgrammingErrorso NetBox can start even when the database is unavailable or migrations haven't run yet. - Bulk action return URL in typed tabs — uses query parameter
?return_url=onformactionfor reliable redirect.
- Templates missing from built wheel — added
[tool.setuptools.package-data]inpyproject.tomlandMANIFEST.inso HTML templates are included when installing from PyPI or a pre-built wheel (fixesTemplateDoesNotExistin Docker deployments).
- Custom Objects tab on NetBox object detail pages (Device, Site, Rack, and any configured model), showing Custom Object instances that reference the viewed object via OBJECT or MULTIOBJECT typed fields.
- Pagination using NetBox's
EnhancedPaginator; respects the user's personal per-page preference and the?per_page=NURL parameter. - Text search (
?q=) filtering results by Custom Object instance display name, Custom Object Type name, and field label. - Type filter dropdown (
?type=<slug>) to narrow results to a single Custom Object Type, populated dynamically from types present in the current result set. - Efficient badge counts — the tab badge on every detail page is computed with
COUNT(*)queries (no full object rows fetched). Full rows are loaded only when the tab itself is opened, keeping detail page loads fast even with thousands of linked custom objects. - Wildcard model registration — the
modelsplugin config setting acceptsapp_label.*to register the tab for every model in an app (e.g.dcim.*,ipam.*). - Third-party plugin model support — any installed Django app (including NetBox
plugins) can be listed in
models; Django's app registry treats them identically to built-in apps. - Default configuration:
['dcim.*', 'ipam.*', 'virtualization.*', 'tenancy.*', 'contacts.*']. - Tab is hidden automatically (
hide_if_empty=True) when no custom objects reference the viewed object. - Configurable tab label and weight — set
labelandweightinPLUGINS_CONFIGto control the tab text and position (defaults:'Custom Objects',2000). - Column sorting — clicking the Type, Object, or Field column headers sorts the table in-memory; a second click toggles direction. Sort state is preserved across filter submissions.
- Value column — shows the actual field value on each Custom Object instance: a link for OBJECT fields, or comma-separated links (truncated at 3) for MULTIOBJECT fields.
- Clickable Type column — the Type column links to the Custom Object Type detail
page when the user has
viewpermission; otherwise renders as plain text. - Permission-gated action buttons — each row has an Edit button (requires
changepermission) and a Delete button (requiresdeletepermission). Users without either permission see no action buttons. - HTMX partial updates — pagination, column sorting, search form submission, and
type-dropdown changes now swap only the table zone in-place, without a full page reload.
The URL is updated via
pushStateso links remain shareable and the browser back button restores the previous filter/page state. - Tags column — each row in the Custom Objects table now shows the tags assigned to
that Custom Object instance as colored badges. Rows with no tags display
—. - Tag filter dropdown (
?tag=<slug>) — a tag dropdown appears in the search bar whenever at least one linked Custom Object has a tag, letting users narrow the table to objects with a specific tag. Tag filtering composes with?q=,?type=, sort, and pagination. Tags are pre-fetched in bulk (prefetch_related('tags')) so there is no N+1 query cost. - Configure Table — a "Configure Table" button in the card header opens a NetBox
modal that lets authenticated users show, hide, and reorder columns (Type, Object,
Value, Field, Tags). Preferences are persisted per-user in
UserConfigundertables.CustomObjectsTabTable.columnsand respected on every subsequent page load, including HTMX partial updates. The Actions column is always visible and cannot be hidden.
- Edit/Delete return URL — after saving an edit or confirming a deletion, NetBox now redirects back to the Custom Objects tab instead of to the Custom Object list page.
- Filter state preserved on return — active filters (
?q=,?type=,?sort=,?dir=,?per_page=,?page=) are retained in the return URL so the user lands back on the same filtered/sorted view after editing or deleting a custom object.