Skip to content

Add a combined "Custom Objects" related tab#482

Open
Kani999 wants to merge 6 commits into
netboxlabs:featurefrom
Kani999:feature/related-object-tabs
Open

Add a combined "Custom Objects" related tab#482
Kani999 wants to merge 6 commits into
netboxlabs:featurefrom
Kani999:feature/related-object-tabs

Conversation

@Kani999
Copy link
Copy Markdown
Contributor

@Kani999 Kani999 commented Apr 24, 2026

What

Adds a single combined Custom Objects tab to every object detail page that is referenced by a Custom Object. The tab lists every Custom Object linking to the object being viewed — across all Custom Object Types and all Object / Multi-object fields (polymorphic and non-polymorphic) — with a count badge, live quicksearch, Type/Tag filters, sortable columns, HTMX pagination, and per-row actions. It works on built-in NetBox models and on custom-object (CO→CO) detail pages alike.

It supersedes the old left-column "Custom Objects linking to this object" panel, which is removed.

Why

Two motivations:

  1. One discoverable surface. Previously a linking relationship surfaced as a static left-column panel with no search, filtering, sorting, or actions. The tab matches the native NetBox child-object list (Device → Interfaces) look and behaviour.
  2. Permission correctness. The old panel rendered linked rows ignoring per-Custom-Object-Type view permissions — a pre-existing info leak. The tab filters by the viewing user's permissions throughout: the linked rows, the Multi-object Value column (polymorphic targets included), and the count badge — and the tab hides entirely when the user may view none of the linked objects.

How it works (no coordination machinery)

  • registry.register_tabs() registers the tab view on every public model (ObjectType.objects.public() — exactly the set a COT Object/Multi-object field can target) at plugin startup, and injects one COT-agnostic URL for custom-object host pages. Display is gated purely by a live per-request, permission-aware badge + hide_if_empty, so a newly-referenced model lights up on the next request with no restart — no middleware, signals, or shared cache.
  • views/combined.py centralises the four reference shapes in reference_q() (the single source of truth), with an .exists() fast path that keeps the badge cheap on the common detail pages that reference nothing.
  • The badge reads the viewing user from NetBox's current_request and restricts the per-field counts, so it reflects the rows the user can actually open (not a higher, unfiltered total).
  • Styling mirrors ObjectChildrenView: controls row above the table, object-list table with th.orderable headers (?sort= / -sort), and a compact, keyboard-accessible per-row dropdown (a real <button> toggle — pencil Edit + Changelog + Delete), permission-gated.

What is intentionally not here

  • Native multi-select bulk actions. Rows are heterogeneous Custom Object Type models (and one object can repeat under several linking fields), so NetBox's single-endpoint bulk edit/delete cannot apply. Per-row actions only.
  • Configure-Table omits the saved-table-config "Save" split (that needs a single ObjectType) — matching native, which only shows it when config_params exist.

Tests

netbox_custom_objects/tests/test_related_tabs.py (31 tests): reference_q shapes + empty-Q-means-skip (including the polymorphic multi-object through-table path), per-row and badge permission filtering, _public_host_model_classes inclusion/exclusion, idempotent registration, and unit tests for the value / search / sort display helpers.

Scope note

This branch is a clean recomposition of the earlier typed + combined + hot-reload exploration into the combined tab only. The typed-per-COT tabs and the hot-reload machinery are deliberately dropped as out of scope for this PR; that exploration is preserved on feature/related-object-tabs-v2.

@CLAassistant
Copy link
Copy Markdown

CLAassistant commented Apr 24, 2026

CLA assistant check
All committers have signed the CLA.

@Kani999 Kani999 force-pushed the feature/related-object-tabs branch from fda8748 to 9d84499 Compare April 27, 2026 06:34
@damsitt
Copy link
Copy Markdown

damsitt commented Apr 27, 2026

Instead of a static list in PLUGINS_CONFIG, a per-COT "Show as dedicated tab" checkbox in the admin UI would be cleaner. High-priority COTs get their own tab; everything else falls back to a consolidated "Custom Objects" tab.

@Kani999
Copy link
Copy Markdown
Contributor Author

Kani999 commented May 4, 2026

@damsitt thanks for the suggestion — done. Replaced typed_tab_slugs with a per-COT Show as dedicated tab BooleanField on CustomObjectType. The toggle is reachable from the COT edit form, bulk-edit, CSV import, the REST API, and shows up as a column on the list view. When ticked, the COT renders its own typed tab on related-object detail pages; when unticked, its objects fall back into the consolidated "Custom Objects" tab. Restart-on-change for now (registration runs once in ready()), with a TODO in tab_views.py for live re-registration as a follow-up.

12 commits, 977feeefd4155b. The typed_tab_slugs plugin-config setting is gone (no deprecation since this PR isn't merged yet — clean break).

@bctiemann
Copy link
Copy Markdown
Contributor

@Kani999 We're working on finalizing the v0.5.0 release of netbox-custom-objects this week. It already has a huge number of major features on-train, but it would be nice to get this one in there too. However, as it was not a stakeholder promise it isn't the end of the world if it has to be deferred to a v0.6.0.

What is your feeling on the readiness? Is this week realistic?

Kani999 added a commit to Kani999/netbox-custom-objects that referenced this pull request May 6, 2026
- __init__.py: call clear_url_caches() after inject_co_urls() in ready()
  so URL resolver picks up injected CO patterns in tests and management
  commands (flagged by CodeRabbit on PR netboxlabs#482)
- combined_tab.html: render non-empty non-None values instead of
  always showing em-dash for non-URL/object fields in the else branch
- combined_tab.html: replace plain edit button with a proper Bootstrap
  dropdown toggle so the action dropdown renders correctly
- README.md: minor wording tweak ("Custom Object Type (COT)")
@Kani999

This comment was marked as outdated.

@Kani999
Copy link
Copy Markdown
Contributor Author

Kani999 commented May 6, 2026

Hi @mcolemann — from my side it feels ready. I've just run through the full smoke test matrix above and all scenarios pass except permission gating (which I haven't had a chance to verify yet, but the underlying logic is standard NetBox — it should just work).

One known limitation worth calling out before merge: if you rename a COT's slug while the server is running, the old dedicated tab continues to appear alongside the new one until the process is restarted. The tab registry picks up the new slug immediately (live toggle works correctly), but the old URL entry stays in the router until the next startup. It's documented in the smoke test table (row 10) and in a Known Limitations comment in tab_views.py. It's an edge case — renaming slugs isn't a common operation — but I wanted to flag it explicitly.

If you or the team can spin up the test data (script + JSON are attached above) and walk through a few scenarios, that would be the fastest path to confirming it's v0.5.0-ready. Happy to address any issues that come up during your review.

@Kani999
Copy link
Copy Markdown
Contributor Author

Kani999 commented May 6, 2026

Permission gating — observed behaviour (smoke test row 11)

Tested with a user who has view: Device only (no permissions on any custom object type).

Base panel (the "Custom Objects" linked-objects panel in the left-side panels area):
Still visible and shows all 5 linked objects. This panel is rendered by the base plugin template extension and doesn't gate on per-COT permissions — it lists everything. That's probably worth a follow-up: ideally it would hide rows the user can't access, but it's existing behaviour, not a regression from this PR.

"Custom Objects" combined tab (the tab injected at the top of the device detail page):
The tab badge shows 1 (correct — there is 1 non-dedicated object linked to this device), but after clicking, 0 rows are rendered. The tab body does respect permissions correctly; it's only the badge count that leaks the existence of an object the user can't see.

Dedicated tabs:
Not visible at all for this user — correct.

Summary:

  • Badge count on the combined tab is a minor info leak (count visible, data not)
  • Base panel ignores per-COT permissions entirely (pre-existing)
  • Dedicated tabs are correctly hidden
  • Tab body correctly renders 0 rows when user lacks permission

Neither issue is introduced by this PR, but worth noting before v0.5.0 ships.

@Kani999 Kani999 force-pushed the feature/related-object-tabs branch from 4dadc29 to 7efcede Compare May 6, 2026 08:16
@Kani999 Kani999 changed the title [WIP] Related object tabs — PoC for discussion Related object tabs May 6, 2026
@bctiemann
Copy link
Copy Markdown
Contributor

bctiemann commented May 6, 2026

@Kani999 Thanks for pushing this forward. I think, in the interest of avoiding too much churn and destabilization, I'd like to defer this to a v0.6.0 release. (But note that doesn't mean it will be a long time before that release; it's just the next one to be cut from the feature branch. Which, note, is what this PR will need to target, not main.)

The main thing I'm worried about is polymorphic object/multiobject fields, which are just about to land in feature and go out with v0.5.0. There is a lot of movement in the code there that will very likely impact this PR/feature, and I'd like polymorphism to settle first to give us a chance to ensure related object tabs are baked well and support polymorphism properly. I don't want to rush this.

@bctiemann bctiemann modified the milestone: Future Minor Release May 6, 2026
@Kani999 Kani999 changed the base branch from main to feature May 7, 2026 06:23
@Kani999 Kani999 force-pushed the feature/related-object-tabs branch from 7efcede to 2ee95e5 Compare May 7, 2026 08:15
Kani999 added a commit to Kani999/netbox-custom-objects that referenced this pull request May 7, 2026
- __init__.py: call clear_url_caches() after inject_co_urls() in ready()
  so URL resolver picks up injected CO patterns in tests and management
  commands (flagged by CodeRabbit on PR netboxlabs#482)
- combined_tab.html: render non-empty non-None values instead of
  always showing em-dash for non-URL/object fields in the else branch
- combined_tab.html: replace plain edit button with a proper Bootstrap
  dropdown toggle so the action dropdown renders correctly
- README.md: minor wording tweak ("Custom Object Type (COT)")
@Kani999
Copy link
Copy Markdown
Contributor Author

Kani999 commented May 7, 2026

Understood, thanks for the context — agreed it's better to land related-object tabs on top of finalized polymorphic field support than to chase a moving target.

I've already retargeted this PR from main to feature and rebased onto the current feature head, so it's MERGEABLE now and the diff reflects only the tab work. I'll keep an eye on #442 and rebase again once polymorphism lands, so this is ready to revisit whenever v0.6.0 is being cut.

@Kani999 Kani999 force-pushed the feature/related-object-tabs branch from b0f88da to d3faf82 Compare May 7, 2026 08:40
@Kani999
Copy link
Copy Markdown
Contributor Author

Kani999 commented May 12, 2026

Heads up — this is not ready to merge yet.

I need to:

  • Rework the code to align with the polymorphic feature
  • Re-run the test suite against the updated implementation
  • Add/adjust tests as needed

I'll push the revised commits and updated test results once that's done.

@jeremystretch jeremystretch marked this pull request as draft May 12, 2026 12:21
@jeremystretch
Copy link
Copy Markdown
Contributor

@Kani999 I've converted this PR to a draft per your note above. Just mark it as "ready for review" when the time comes. Thanks!

@Kani999
Copy link
Copy Markdown
Contributor Author

Kani999 commented May 26, 2026

This branch has been completely reworked on top of upstream main (v0.5.1 + #524) with full polymorphic Object/Multi-object field support. The PR description above has been updated to reflect the new implementation — please review against the current description rather than the original one.

Kani999 added a commit to Kani999/netbox-custom-objects that referenced this pull request May 26, 2026
GET /api/plugins/custom-objects/custom-object-types/?slug=foo previously
returned every COT — django-filter silently dropped the unrecognised
query parameter because the filterset's Meta.fields didn't include slug.
Adding slug to the tuple lets django-filter auto-generate the full
lookup family (exact, __ic, __isw, __regex, ...) backed by the
CustomObjectType.slug SlugField.

Verified via manage.py shell:

    from netbox_custom_objects.filtersets import CustomObjectTypeFilterSet
    'slug' in CustomObjectTypeFilterSet.base_filters
    True

Side observation from PR netboxlabs#482's polymorphic smoke run on 2026-05-26.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@Kani999 Kani999 force-pushed the feature/related-object-tabs branch from 9e0de1e to 40eb1f7 Compare May 26, 2026 12:10
@Kani999 Kani999 force-pushed the feature/related-object-tabs branch 2 times, most recently from 3234e2a to f9f67ea Compare May 26, 2026 12:51
@Kani999
Copy link
Copy Markdown
Contributor Author

Kani999 commented May 27, 2026

@bctiemann

Quick thought / question on nbco:tab_registry_version (Redis-based hot-reload)

Before this lands I'd like to get your gut feel on the Redis-counter hot-reload mechanism, because I'm honestly a bit uncomfortable with the complexity it adds and I'd rather hear "yeah, that's fine" or "no, please simplify" from you now than after merge.

Why this mechanism exists at all. Unlike core NetBox tabs, the tabs this PR introduces depend on database state (CustomObjectType rows + show_dedicated_tab), which users can change at runtime. NetBox builds the tab registry and URL conf once in ready(), so without some kind of cross-process signal, a PATCH show_dedicated_tab=true on one worker would not become visible on other workers until a NetBox restart. That's a pretty rough UX for a feature whose whole point is runtime-defined object types.

What I did. A small monotonic counter in Redis (nbco:tab_registry_version) + a thin middleware that GETs it once per request and rebuilds the local registry if it's behind. Cost on the steady-state hot path is ~1 ms (one Redis GET).

What bugs me. It's still a piece of distributed state that has to stay in sync with the DB, every request pays a Redis GET, and the ViewTab.visible() + startup seeding are defence-in-depth specifically because there are more moving parts than I'd like.

What I tried. I built a side branch that strips the Redis machinery and refreshes the registry only in the process that handled the mutation — diff here, single commit 3366144. I deployed it on a server running granian with multiple workers and confirmed the failure mode empirically: after toggling show_dedicated_tab=true, reloading the related detail page shows the tab on some reloads and not on others — the tab is visible only when the request happens to hit the worker that handled the PATCH, and invisible on every other worker. So without the Redis counter + middleware I can't get hot-reload to propagate to all workers; a NetBox restart is the only way to converge them.

What I'd love your input on.

  1. Is there an existing primitive in NetBox/Django land for "tell all workers to invalidate something" that I missed? (I looked but didn't find one.)
  2. If not, are you OK with the Redis-counter approach as it stands, or would you rather accept the restart-required tradeoff and drop the whole mechanism?
  3. Or — third option — is there a different angle entirely (lazy registry rebuild on miss, periodic poll, a TTL on the registry, …) that you'd prefer?

Happy to go any direction. I just don't want to silently merge in a non-trivial coordination primitive without you having had a chance to weigh in.

@bctiemann
Copy link
Copy Markdown
Contributor

@Kani999 My first reaction is that dealing with cache invalidation across multiple workers has been one of the biggest persistent issues in Custom Objects ever since the cache (whose purpose is to prevent infinite recursion during get_model invocations of COTs with object/multiobject fields) was introduced. One of the thing we tried hard to avoid having to introduce was a shared cache backend, i.e. in Redis, and thus far we've been successful at that.

What we've had to do in recent changes (in the 0.5.0 cycle) is introduce caching techniques with versioning/timestamps that force different workers to invalidate their caches. I don't have the details at top of mind, but if you're using Claude to introspect the cache machinery, I'd prompt it with the challenge to use a similar technique to solve this problem — see whether the dynamic model cache can be manipulated when tab display needs to be updated, using the same existing techniques for ensuring cache freshness across workers that are used when COTFs are updated.

@Kani999 Kani999 changed the base branch from main to feature May 29, 2026 06:16
@Kani999
Copy link
Copy Markdown
Contributor Author

Kani999 commented May 29, 2026

@bctiemann — done, and thanks, that was the right instinct.

I dropped the Redis counter entirely and rebuilt hot-reload on the cache_timestamp versioning the dynamic-model cache already uses. Each worker keeps a (MAX(cache_timestamp), COUNT(*)) snapshot over CustomObjectType; a thin middleware compares it per request and re-registers tabs only when it drifts. MAX catches creates/updates (every COT + COTField save bumps cache_timestamp, including the m2m_changed path for polymorphic targets); COUNT catches deletes. No shared cache backend — the same DB column that gates the model cache gates the tab registry, so the invariant lives in one place.

@Kani999
Copy link
Copy Markdown
Contributor Author

Kani999 commented May 29, 2026

Stepping back, though: even on cache_timestamp, the hot-reload subsystem is heavy — middleware on every request, signal handlers, registry purge/re-register, and the part I trust least, _inject_host_typed_tab_urls() rewriting host apps' frozen urlpatterns in place. It works, but it's a lot of moving parts for a tabs feature, and this PR's size reflects that.

I think we can delete the entire mechanism by splitting the feature in two:

Step 1 — combined "Custom Objects" tab only. One tab per host model; badge/contents/filters all read from the DB per request. No middleware / signals / URL-injection / cache token. Branch: feature...feature/related-object-tabs-combined — a clean ~+1.1k-line pure addition, zero model/migration changes. It's purely additive: it does not remove or change the existing "Custom Objects linking to this object" panel (template_content.py) — both surface the same relationships, so if we go this way we should decide whether the tab supersedes that panel or they coexist.

Liveness, by host kind:

  • References between custom object types (CO→CO) are always live — no restart. Custom-object detail pages are rendered by the plugin's own template, so the tab nav-link is computed live from the DB per render, and its URL is a single COT-agnostic route (…/<cot-slug>/<pk>/custom-objects/, slug as a path param) injected once at startup that reverses for any slug — including COTs created later. (Smoke-tested: a brand-new COT referencing another shows the tab immediately, badge updates live.)
  • Built-in NetBox hosts (Device, Site, …): live for everything except the first-ever reference to a NetBox model type nothing referenced at startup — that model's per-model tab URL is frozen into the URLconf at boot, so it needs a restart to appear. (No manage.py command avoids this: a separate process can't mutate live workers' URLconf; only shared polling — what we're deleting — or a restart crosses the process boundary.)
  • Variant B removes even that built-in caveat: register the tab on all models at startup and gate display by the live DB badge → fully live everywhere, still zero machinery, at the cost of a cheap "is this referenced?" check on every detail page.

Everyday usage (new COTs against existing targets, new objects, edits) is live under every variant.

Step 2 — per-COT typed tabs (separate top-level tabs), as a focused follow-up. These give each opted-in COT its own dedicated tab on the host page (native columns, per-field filters, bulk actions). They're the part that genuinely needs runtime URL registration — each typed tab requires its own reversible URL name, which is exactly what drove the hot-reload / URL-injection machinery in this PR. I'd land them on top of the combined tab once it's in, and decide then between liveness options for them specifically.

Honestly I lean toward shipping Step 1 (combined tab) first — it's the least noisy, lands the core value, and lets typed tabs come as a focused follow-up rather than carrying the full machinery in one merge. What's your call on the built-in liveness (accept the restart caveat vs. Variant B), and on combined-first vs keeping it all in one PR?

@bctiemann
Copy link
Copy Markdown
Contributor

bctiemann commented May 29, 2026

I do like the sound of Step 1 (with the Variant B -- the tradeoff of a "is this referenced?" check (if the plugin is even present?) versus having to worry about the "first time referenced after startup" business seems fine to me).

Step 2 may or may not even be necessary or advisable as I understand it -- there can be an arbitrary number of different COTs linking to a core object, which means an arbitrary number of tabs, right? I had thought the shape this was going to take was just a single "Custom Objects" tab that aggregated everything the same way the "Custom Objects linking to this object" card does now. Either way it does seem like a value-add that I'd be happy to split off as a later improvement.

@Kani999 Kani999 force-pushed the feature/related-object-tabs branch from 3eaacbf to 863431b Compare June 4, 2026 09:57
@Kani999
Copy link
Copy Markdown
Contributor Author

Kani999 commented Jun 4, 2026

Rescoped 🧹

I've force-pushed this branch to a clean combined-only history (5 commits).

The earlier exploration carried three things — per-COT typed tabs, the combined tab, and hot-reload machinery — and the combined tab alone delivers the user-facing value without the coordination complexity (no middleware/signals/shared cache; everything is live per-request).

What changed vs. the previous push:

  • Dropped the typed-per-COT tabs and the hot-reload/registry-rebuild machinery.
  • Kept + polished the combined "Custom Objects" tab; restyled it to match the native ObjectChildrenView (Device → Interfaces) look.
  • Removed the old "Custom Objects linking to this object" panel (superseded).

The original typed-tabs work is preserved at feature/related-object-tabs-v2 if we want to revisit it as a follow-up.

History (on top of feature):

  1. add combined "Custom Objects" tab
  2. supersede the old linking panel
  3. tests
  4. README
  5. (unrelated) fix literal {toggle_text}/{attrs.title} placeholders in the COT fields actions column

@Kani999 Kani999 changed the title Related object tabs Add a combined "Custom Objects" related tab Jun 4, 2026
@Kani999 Kani999 force-pushed the feature/related-object-tabs branch 2 times, most recently from 0efa86e to f3e758a Compare June 5, 2026 08:10
Kani999 added 6 commits June 5, 2026 11:07
Add a single "Custom Objects" tab to the detail page of any NetBox object
referenced by a Custom Object Type field. It lists every custom object linking
to the object — across all types and Object/Multi-object fields, polymorphic or
not — with a count badge, quicksearch, type/tag filters, sortable columns, HTMX
pagination, and per-row actions, styled like a native child-object list.

The design avoids any coordination machinery (no middleware, signals, or shared
cache): the tab view is registered on every public model at startup and gated
per request by a live badge, so a newly-referenced model's tab appears on the
next request with no restart. Custom-object (CO->CO) hosts are served by one
slug-agnostic URL plus a live nav-link, so references between types are live too.

Permissions are enforced throughout: linked rows, the multi-object Value column
(polymorphic targets included), and the badge are all filtered to what the
viewing user may see, and the tab hides when they can view none.
The combined tab renders the same relationships as the old "Custom Objects
linking to this object" left-column panel, so the panel is redundant. The tab is
strictly better: it enforces per-type view permissions on the rows it lists (the
panel ignored them — a pre-existing info leak) and adds search, filters,
sorting, and per-row actions.

Removes CustomObjectLink/LinkedCustomObject, the now-dead LinkedCustomObjectTable,
and the panel-only test; tab coverage lives in tests/test_related_tabs.py.
Cover the surfaces most likely to regress: reference_q's four reference shapes
and its empty-Q-means-skip guard (a regression there would leak every custom
object onto every host page); per-row and badge permission filtering, including
the polymorphic multi-object through path; host-model enumeration and idempotent
registration; and the value/search/sort display helpers.
Describe the auto-discovered tab — that it supersedes the old linking panel,
enforces per-type view permissions, and is fully live (no restart).
customobjecttype.html copied the field-actions dropdown markup from
CustomObjectTypeFieldTable but left two f-string placeholders unconverted, so
they rendered literally: aria-label="{attrs.title}" and the {toggle_text}
toggle. Replace with {% trans %} tags. Pre-existing (upstream 8b3e74a),
unrelated to the combined tab — kept separate so it can be split out.
Two query-volume fixes for the combined "Custom Objects" tab, both confined to
the tab's own code paths.

Batch the Value column for non-polymorphic multi-object fields. Resolving each
row's targets through manager.all() issued one through-table query plus one
target query per row — an N+1 that dominated the tab's query count on pages
full of multi-object rows. _batch_multiobject_values now prefetches each
(model, field) group on the page in a single pass via the custom M2M manager's
get_prefetch_querysets and reads the result from the prefetch cache, filtering
targets by view permission in Python (as the polymorphic path already does).

Memoize the linked-field discovery per request. The body render and the ViewTab
badge both walked _iter_linked_fields, which calls get_model() once per linked
type — so every detail page regenerated those models twice. _linked_fields
caches the (field, model, q) triples on the request object, collapsing the two
passes into one; it falls back to a fresh build when there is no request
context. The triples are user-independent, so the per-row .restrict() in each
caller still enforces permissions.

On a device referenced by ~20 custom object types this cut the tab from 224 to
177 SQL queries and roughly halved SQL time. Adds a regression test asserting
multi-object value resolution stays constant in query count as rows grow.
@Kani999 Kani999 force-pushed the feature/related-object-tabs branch from f3e758a to 7c3d9e6 Compare June 5, 2026 10:57
@Kani999
Copy link
Copy Markdown
Contributor Author

Kani999 commented Jun 5, 2026

Pushed a follow-up that removes two sources of redundant queries in the combined tab, both scoped to the tab's own code paths:

  • Multi-object Value column N+1. Non-polymorphic multi-object fields were resolved one row at a time (manager.all() → one through-table query + one target query per row). They're now prefetched once per (model, field) group on the page via the custom M2M manager's get_prefetch_querysets, with permission filtering done in Python (as the polymorphic path already does).
  • Linked-field discovery. The body render and the ViewTab badge both walked _iter_linked_fields, which calls get_model() per linked type — so each page regenerated those models twice. It's now memoized on the request object (with a fresh-build fallback when there's no request context). The cached triples are user-independent, so the per-row .restrict() in each caller still enforces permissions.

Added a regression test asserting multi-object value resolution stays constant in query count as rows grow.

Out of scope here: the plugin's navigation menu regenerates every dynamic model on each page render (CustomObjectTypeMenuItems.__iter__get_model() per type), which remains the dominant per-page query cost. It's best addressed separately — e.g. request-scoped get_model() memoization shared across the menu, tab, and badge.

@Kani999 Kani999 marked this pull request as ready for review June 5, 2026 11:04
@Kani999
Copy link
Copy Markdown
Contributor Author

Kani999 commented Jun 5, 2026

@bctiemann I think this is ready for review now. The combined "Custom Objects" tab is feature-complete — count badge, quicksearch, type/tag filters, sortable columns, HTMX pagination, per-row actions, and per-user permission filtering throughout.

Let me know if you'd like anything changed.

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.

Auto generation of tabs in other objects that lists related Custom Objects

5 participants