Add polymorphic Object/MultiObject field support + release 2.4.0#13
Merged
Kani999 merged 5 commits intoMay 13, 2026
Conversation
#12) netbox-custom-objects v0.5.0 introduced is_polymorphic=True fields whose targets live in related_object_types (M2M) instead of related_object_type (FK), and per-field through tables keyed by (source_id, content_type_id, object_id). Pre-2.4.0 discovery filtered only on related_object_type, so polymorphic links were invisible: tabs disappeared from referenced hosts until the field was switched back to non-polymorphic. Both tab styles now mirror upstream CustomObjectLink.left_page: a non-poly queryset plus a polymorphic queryset, branching into four reverse-lookup shapes (FK column, GFK pair, M2M, polymorphic through table). The typed-tab "Add <Type>" toolbar prefills upstream's polymorphic sub-field widget names (?<name>__ct/__obj for poly Object, ?<name>__<app>__<model> for poly MultiObject); previously the Add link was silently skipped for poly fields. Min upstream raised to 0.5.0, enforced at startup: PluginConfig.ready() probes for the is_polymorphic model field and raises ImproperlyConfigured with a clear upgrade message if upstream is older. Behaviour-based check (field probe, not version string) so it stays correct across forks and pre-release tags. Pre-0.5 compat shims removed from combined.py / typed.py. README and CHANGELOG known-issues sections updated to call out the upstream delete-bug class: affects netbox-custom-objects == 0.5.0, fixed in upstream main (PR #501, merged 2026-05-11) and the forthcoming 0.5.1 release. Bulk Delete is NOT a workaround (corrects 2.3.0 README claim) — NetBox's BulkDeleteView.post iterates and calls obj.delete() per row, the same code path. README documents the recommended fix (upgrade upstream) plus shell-based and restart workarounds for installs that cannot upgrade immediately.
The 2.4.0 polymorphic-field refactor changed three function signatures and split CustomObjectTypeField lookups into two querysets (non-polymorphic FK + polymorphic M2M), which broke 15 mock-based unit tests: - _count_for_type gained a required host_ct_id; pass it in all four TestCountForType cases. - _build_add_links now takes a host_instance (needs .pk, ._meta) instead of a bare pk; add a _make_host helper and an autouse fixture that patches ContentType so the unconditional get_for_model call doesn't reach the ORM. - _make_typed_tab_view's signature gained host_ct_id as the 5th positional; update fake_make_view to accept it so register_typed_tabs doesn't raise TypeError before reaching the captured field_infos. - _iter_linked_fields (combined.py) and register_typed_tabs (typed.py) now call CustomObjectTypeField.objects.filter() twice; replace the shared return_value mock with side_effect=[non_poly_qs, poly_qs] so the poly path doesn't reuse the non-poly list (which caused doubled counts in combined and "list has no attribute prefetch_related" in typed).
View.get() built q_filter as Q() and OR'd in per-field predicates without tracking whether any non-empty Q was produced. An empty Q() is the identity element of `|`, so dynamic_model.objects.filter(Q()) is equivalent to .all() — if every _build_q_for_field call returned an empty Q (unresolvable through model, unknown field type), the tab would silently widen to every row of the target Custom Object Type instead of being empty. In practice register_typed_tabs only registers tabs for COTs with at least one OBJECT/MULTIOBJECT field referencing the host CT, so the bug is latent rather than active — but the guard pattern was already present in _count_for_type and was inconsistent with the view body. Mirror the has_filter pattern: short-circuit to dynamic_model.objects.none() when no field produced a non-empty Q. Verified via nbshell: 3 happy paths (non-poly FK, polymorphic GFK, polymorphic through-table M2M) still return correct row counts; defensive path returns .none() (0 rows) where the pre-fix filter(Q()) would have returned all rows of the type. Flagged by CodeRabbit (warning, views/typed.py:309-316).
4 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #12.
Summary
netbox-custom-objectsv0.5.0 introducedis_polymorphic=TrueObject / MultiObject fields whose targets live inrelated_object_types(M2M) instead ofrelated_object_type(FK), with per-field through tables keyed by(source_id, content_type_id, object_id)for the MultiObject variant. Pre-2.4.0 discovery 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
CustomObjectLink.left_page— a non-polymorphic queryset plus a polymorphic queryset, with per-field branching into four reverse-lookup shapes: FK column, GFK(content_type_id, object_id)pair, M2M, polymorphic through table (subquery).Changes
netbox_custom_objects_tab/views/combined.py—_iter_linked_fieldsfetches both non-poly and poly querysets, yields the four reverse-lookup shapes.netbox_custom_objects_tab/views/typed.py—register_typed_tabsfans polymorphic fields out acrossrelated_object_types.all()while keeping the(host_ct_id, cot_id)group key;_build_q_for_fieldcentralises the 4-way branch;_build_add_linksre-enabled for polymorphic fields using upstream's sub-field prefill format (?<name>__ct=…&<name>__obj=…for poly Object,?<name>__<app>__<model>=…for poly MultiObject — the previous behaviour silently skipped the Add link for polymorphic fields).netbox_custom_objects_tab/__init__.py— runtimeImproperlyConfiguredgate inPluginConfig.ready()probes for theis_polymorphicmodel field and raises a clear upgrade message if upstream is older than 0.5.0. Behaviour-based check (field probe, not version string) so it stays correct across forks and pre-release tags.combined.pyandtyped.py(getattrguards, module feature probes).pyproject.toml—2.3.0→2.4.0.CHANGELOG.md+README.md— release entry and updated "Known Issues" section (see below).Compatibility
netbox_custom_objectsversionThe minimum-upstream bump is enforced at startup, not via
pyproject.tomldependencies— runtimeImproperlyConfiguredwith an explicit upgrade message gives users a much clearer signal than a pip resolver error.Known issue called out in README + CHANGELOG
Deleting a Custom Object via the NetBox UI on
netbox-custom-objects == 0.5.0can raise:from
CustomObjectDeleteView._get_dependent_objects. Fixed in upstreammainby PR #501 (merged 2026-05-11) and will ship in the forthcoming0.5.1release. Bulk Delete is not a workaround (NetBox's genericBulkDeleteView.post()iterates and callsobj.delete()per row — same code path; corrects the 2.3.0 README claim). This plugin doesn't override delete or model caching and cannot patch the bug from its own code; the recommended resolution is to upgradenetbox-custom-objectsto a build containing PR #501. README documents the upgrade path plusmanage.py shelland restart workarounds for installs that cannot upgrade immediately.Adjacent fixes that also land in upstream
main/0.5.1and close the same drift-prone class: PR #504 (cross-COT FK fields after restart), PR #505 (stale through-model FK path_infos on COT regeneration), PR #510 (self-referential FK isinstance check). Upgrading once closes the family.Test plan
ruff checkcleanruff format --checkcleanmanage.py checkclean (only the documentedAPPS_NOT_READYwarning)?<name>__ct&__obj) and polymorphic MultiObject (?<name>__<app>__<model>) fieldsImproperlyConfiguredraised at startup onnetbox-custom-objects < 0.5.0netbox_custom_objects.*in config)Notes
pyproject.tomldependenciesdeliberately left empty fornetbox-custom-objects; the runtimeImproperlyConfiguredcheck is the single point of enforcement (per established project convention).