diff --git a/CHANGELOG.md b/CHANGELOG.md index 7537bed..0f0e4c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,75 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [2.3.0] - 2026-05-12 + +### Added + +- **Add button on Typed tabs** ([#9](https://github.com/CESNET/netbox-custom-objects-tab/issues/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_add` view with the reverse-reference field pre-filled to the + parent object's PK and `return_url` set 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_device` and `backup_device` both → Device), the button + becomes a split-dropdown listing each field. The button is hidden for + users without `add_customobject` permission. + +### Fixed + +- **Typed-tab URL registration**: typed-tab views are now registered + synchronously inside `AppConfig.ready()` instead of from a `request_started` + signal handler. The earlier deferral (commit `5bf09c3`, PR #4) silenced + some startup warnings but broke typed-tab routing entirely — NetBox's + `get_model_urls()` snapshots `registry['views']` when each model's + `urls.py` is 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 with + `typed_models` configured. The `OperationalError`/`ProgrammingError` + safety net inside `register_typed_tabs` still covers the + `manage.py migrate` / fresh-DB case. +- **Typed-tab badge no longer over-counts** rows that match the parent via + multiple fields. `_count_for_type` previously 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_devices` all → `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 same + `Q-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_customobject` respectively, matching the gating pattern the + Add button uses. Previously these buttons rendered unconditionally on + Typed tabs; clicks were rejected server-side by NetBox's + `customobject_bulk_*` views but the unguarded UI render was confusing + for non-superusers. Per-button guards (rather than gating the whole + toolbar on `change AND delete`) so a user with only `change` perm + sees Bulk Edit but not Bulk Delete, and vice versa. Surfaced by the + 2.3.0 smoke test with non-admin test users. + +### Known Issues + +- **Upstream `netbox_custom_objects` bug 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) + raises `ValueError: Cannot query "X": Must be "TableModel" instance.` + from `CustomObjectDeleteView._get_dependent_objects` (upstream + `netbox_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 in `netbox_custom_objects`, not in this plugin. + See README "Known Issues" section for user-facing workarounds. + ## [2.2.0] - 2026-05-11 ### Changed diff --git a/CLAUDE.md b/CLAUDE.md index ada6a69..f6c0e12 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -209,6 +209,7 @@ permissions internally via `get_permission_for_model()`. - Typed tabs use `custom-objects-{slug}` path prefix — avoids collisions with built-in paths - Multiple fields of same type → union querysets with `.distinct()` - Tabs registered at `ready()` — new Custom Object Types need a restart (applies both to typed tabs on native models and to `netbox_custom_objects.*` tabs on Custom Object pages) +- **Do NOT defer typed-tab registration to `request_started` or any post-`ready()` signal.** NetBox's `get_model_urls(app, model)` snapshots `registry['views']` when the model's `urls.py` is first imported (which happens lazily on the first `resolve()` call). Anything added to the registry after that has no URL pattern. Combined tabs work because they're registered in `ready()` synchronously; typed tabs MUST be registered the same way. PR #4 / commit `5bf09c3` deferred typed-tab registration to silence DB-access startup warnings — that change broke typed tabs entirely and was reverted in 2.3.0. The DB-access warning is acceptable; broken URL routing is not. - `netbox_custom_objects.*` wildcard is special-cased in `_resolve_model_labels()` — dynamic models are discovered via `app_config.get_models()` filtered to `CustomObject` subclasses. **Do NOT call `get_model()` here** — each cache-miss call re-registers journal/changelog tab views, producing duplicate tabs - `base_template` for CO model instances must be `netbox_custom_objects/customobject.html` — the per-model template (e.g. `netbox_custom_objects/table28model.html`) does not exist - Tab view `get()` must accept `**kwargs` — CO detail URLs pass `custom_object_type` slug as an extra kwarg alongside `pk` diff --git a/README.md b/README.md index 42f7593..1f0b3ad 100644 --- a/README.md +++ b/README.md @@ -208,6 +208,39 @@ The tab displays: | **Tags** | Colored tag badges assigned to the Custom Object instance; `—` when none | | *(actions)* | Edit and Delete buttons, each shown only when the user has the corresponding permission | +## Known Issues + +### Per-row Delete fails on the first attempt right after Create (upstream bug) + +After creating a custom object via the 2.3.0 "Add *Type*" button on a +Typed tab, clicking the per-row **Delete** action in the list **on the +very first attempt** raises a `ValueError` inside upstream +`netbox_custom_objects.CustomObjectDeleteView`: + +``` +ValueError: Cannot query "": Must be "TableModel" instance. +``` + +(at `netbox_custom_objects/views.py:977`, inside +`_get_dependent_objects`). Workarounds: + +1. **Refresh the typed-tab list page** between clicking Create and + clicking the per-row Delete. The second `/delete/` GET succeeds. +2. **Use Bulk Delete** instead — it goes through a different upstream + code path and is unaffected. + +Pre-existing rows (created in earlier sessions or via the upstream +"Add" menu under Custom Objects → *Type*) are not affected. The bug +originates in dynamic-model class identity drift across the +Create → Delete request boundary in the upstream `netbox_custom_objects` +plugin: each Custom Object Type backs a dynamically-generated Django +model (`TableModel`), the model class registry rebuilds during the +Create POST, and the immediately-following Delete GET still holds a +reference to the prior class object in some scope (queryset cache, +prefetch, or import-level reference) until a request boundary refreshes +it. Will be tracked and fixed upstream; this plugin's 2.3.0 release +ships with the workaround documented here. + ## Support - Open an issue on [GitHub](https://github.com/CESNET/netbox-custom-objects-tab/issues) diff --git a/netbox_custom_objects_tab/templates/netbox_custom_objects_tab/typed/tab.html b/netbox_custom_objects_tab/templates/netbox_custom_objects_tab/typed/tab.html index 3ad9097..71e59a0 100644 --- a/netbox_custom_objects_tab/templates/netbox_custom_objects_tab/typed/tab.html +++ b/netbox_custom_objects_tab/templates/netbox_custom_objects_tab/typed/tab.html @@ -5,7 +5,6 @@ {% load i18n %} {% block content %} {% if table %} -
{# Results / Filters inner tabs #}