Skip to content

Add polymorphic Object/MultiObject field support + release 2.4.0#13

Merged
Kani999 merged 5 commits into
masterfrom
12-feature-support-polymorphic-object-multiobject-fields-from-netbox-custom-objects-v050
May 13, 2026
Merged

Add polymorphic Object/MultiObject field support + release 2.4.0#13
Kani999 merged 5 commits into
masterfrom
12-feature-support-polymorphic-object-multiobject-fields-from-netbox-custom-objects-v050

Conversation

@Kani999
Copy link
Copy Markdown
Collaborator

@Kani999 Kani999 commented May 13, 2026

Closes #12.

Summary

netbox-custom-objects v0.5.0 introduced is_polymorphic=True Object / MultiObject fields whose targets live in related_object_types (M2M) instead of related_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-FK related_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_fields fetches both non-poly and poly querysets, yields the four reverse-lookup shapes.
  • netbox_custom_objects_tab/views/typed.pyregister_typed_tabs fans polymorphic fields out across related_object_types.all() while keeping the (host_ct_id, cot_id) group key; _build_q_for_field centralises the 4-way branch; _build_add_links re-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 — runtime ImproperlyConfigured gate in PluginConfig.ready() probes for the is_polymorphic model 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.
  • Pre-0.5 compat shims removed from combined.py and typed.py (getattr guards, module feature probes).
  • pyproject.toml2.3.02.4.0.
  • CHANGELOG.md + README.md — release entry and updated "Known Issues" section (see below).

Compatibility

Plugin version NetBox version netbox_custom_objects version
2.4.x 4.5.4+ / 4.6.x ≥ 0.5.0 required (≥ 0.5.1 strongly recommended — fixes Delete bug)
2.3.x 4.5.4+ / 4.6.x ≥ 0.4.6 (≥ 0.5.0 on 4.6)

The minimum-upstream bump is enforced at startup, not via pyproject.toml dependencies — runtime ImproperlyConfigured with 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.0 can raise:

ValueError: Cannot query "<row title>": Must be "Table<N>Model" instance.

from CustomObjectDeleteView._get_dependent_objects. Fixed in upstream main by PR #501 (merged 2026-05-11) and will ship in the forthcoming 0.5.1 release. Bulk Delete is not a workaround (NetBox's generic BulkDeleteView.post() iterates and calls obj.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 upgrade netbox-custom-objects to a build containing PR #501. README documents the upgrade path plus manage.py shell and restart workarounds for installs that cannot upgrade immediately.

Adjacent fixes that also land in upstream main / 0.5.1 and 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 check clean
  • ruff format --check clean
  • manage.py check clean (only the documented APPS_NOT_READY warning)
  • Programmatic smoke verifier: 29 / 29 PASS
  • Browser smoke test, Phases 1–10a/10b + 11–14: 13 / 13 mandatory PASS
  • Polymorphic Object field: tab appears on each allowed host type (e.g. Device + Interface), links resolve to the right CO instances
  • Polymorphic MultiObject field: tab appears on each allowed host, through-table reverse lookup returns the correct rows
  • Typed tab "Add Type" toolbar prefills the correct host on polymorphic Object (?<name>__ct&__obj) and polymorphic MultiObject (?<name>__<app>__<model>) fields
  • ImproperlyConfigured raised at startup on netbox-custom-objects < 0.5.0
  • No regression on non-polymorphic Object / MultiObject fields (combined tab + typed tab still render, filter, sort, paginate)
  • No regression on CO→CO tabs (netbox_custom_objects.* in config)
  • Delete-cycle verified PASS on a build containing upstream PR #501; the documented bug class is confirmed upstream-only

Notes

  • No database migrations (this plugin has no models).
  • No template-context-processor or middleware additions.
  • pyproject.toml dependencies deliberately left empty for netbox-custom-objects; the runtime ImproperlyConfigured check is the single point of enforcement (per established project convention).

 #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).
@Kani999 Kani999 merged commit 4edd86f into master May 13, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature] Support polymorphic Object / MultiObject fields from netbox-custom-objects v0.5.0

2 participants