@@ -27,16 +27,26 @@ Two tab modes are available:
2727## Requirements
2828
2929- NetBox 4.5.0 – 4.6.99
30- - ` netbox_custom_objects ` plugin ** ≥ 0.4.6** installed and configured (≥ 0.5.0 recommended on NetBox 4.6)
30+ - ` netbox_custom_objects ` plugin ** ≥ 0.5.0** installed and configured
31+ (** ≥ 0.5.1 strongly recommended** — 0.5.0 has an upstream Delete bug
32+ that 0.5.1 fixes; see [ Known Issues] ( #known-issues ) )
3133
3234## Compatibility
3335
34- | Plugin version | NetBox version | ` netbox_custom_objects ` version |
35- | ----------------| ----------------| ---------------------------------------|
36- | 2.2.x | 4.5.4+ / 4.6.x | ≥ 0.4.6 (≥ 0.5.0 on 4.6) |
37- | 2.1.x | 4.5.4+ | ≥ 0.4.6 |
38- | 2.0.x | 4.5.x | ≥ 0.4.6 |
39- | 1.0.x | 4.5.x | ≥ 0.4.4 |
36+ | Plugin version | NetBox version | ` netbox_custom_objects ` version |
37+ | ----------------| ----------------| ------------------------------------------------------------------------|
38+ | 2.4.x | 4.5.4+ / 4.6.x | ** ≥ 0.5.0 required** (≥ 0.5.1 strongly recommended — fixes Delete bug) |
39+ | 2.3.x | 4.5.4+ / 4.6.x | ≥ 0.4.6 (≥ 0.5.0 on 4.6) |
40+ | 2.2.x | 4.5.4+ / 4.6.x | ≥ 0.4.6 (≥ 0.5.0 on 4.6) |
41+ | 2.1.x | 4.5.4+ | ≥ 0.4.6 |
42+ | 2.0.x | 4.5.x | ≥ 0.4.6 |
43+ | 1.0.x | 4.5.x | ≥ 0.4.4 |
44+
45+ Plugin 2.4.x ** enforces** the 0.5.0 minimum at startup: ` PluginConfig.ready() `
46+ probes for the upstream ` is_polymorphic ` model field and raises
47+ ` ImproperlyConfigured ` with an upgrade message if the installed upstream is
48+ older. The check is behaviour-based (looks for the field, not a version
49+ string) so it stays correct across forks and pre-release tags.
4050
4151## Installation
4252
@@ -210,36 +220,110 @@ The tab displays:
210220
211221## Known Issues
212222
213- ### Per-row Delete fails on the first attempt right after Create (upstream bug )
223+ ### Upstream Delete bug on ` netbox-custom-objects == 0.5.0 ` (fixed in 0.5.1 )
214224
215- After creating a custom object via the 2.3.0 "Add * Type* " button on a
216- Typed tab, clicking the per-row ** Delete** action in the list ** on the
217- very first attempt** raises a ` ValueError ` inside upstream
225+ ** Affected versions:** ` netbox-custom-objects == 0.5.0 ` only.
226+ ** Fixed in:** ` netbox-custom-objects ` ` main ` (PR
227+ [ #501 ] ( https://github.com/netboxlabs/netbox-custom-objects/pull/501 ) ,
228+ merged 2026-05-11) and the forthcoming ` 0.5.1 ` release.
229+ ** Not affected:** ` 0.4.x ` (no polymorphic through-models) and any build
230+ that contains PR #501 .
231+
232+ Deleting a Custom Object instance through the NetBox UI on a 0.5.0
233+ install can raise a ` ValueError ` inside
218234` netbox_custom_objects.CustomObjectDeleteView ` :
219235
220236```
221237ValueError: Cannot query "<row title>": Must be "Table<N>Model" instance.
222238```
223239
224- (at ` netbox_custom_objects/views.py:977 ` , inside
225- ` _get_dependent_objects ` ). Workarounds:
226-
227- 1 . ** Refresh the typed-tab list page** between clicking Create and
228- clicking the per-row Delete. The second ` /delete/ ` GET succeeds.
229- 2 . ** Use Bulk Delete** instead — it goes through a different upstream
230- code path and is unaffected.
231-
232- Pre-existing rows (created in earlier sessions or via the upstream
233- "Add" menu under Custom Objects → * Type* ) are not affected. The bug
234- originates in dynamic-model class identity drift across the
235- Create → Delete request boundary in the upstream ` netbox_custom_objects `
236- plugin: each Custom Object Type backs a dynamically-generated Django
237- model (` Table<N>Model ` ), the model class registry rebuilds during the
238- Create POST, and the immediately-following Delete GET still holds a
239- reference to the prior class object in some scope (queryset cache,
240- prefetch, or import-level reference) until a request boundary refreshes
241- it. Will be tracked and fixed upstream; this plugin's 2.3.0 release
242- ships with the workaround documented here.
240+ (at ` netbox_custom_objects/views.py:977 ` , inside ` _get_dependent_objects ` ,
241+ called by Django's ` Collector.collect() ` ). The same crash also occurs from
242+ the bulk-delete view (` CustomObjectBulkDeleteView ` ) because NetBox's
243+ generic ` BulkDeleteView.post() ` iterates the queryset and calls ` obj.delete() `
244+ per row — the same code path. ** Bulk Delete is NOT a workaround**
245+ (earlier versions of this README claimed it was; that was incorrect).
246+
247+ #### Recommended fix — upgrade upstream
248+
249+ The cleanest resolution is to upgrade ` netbox-custom-objects ` to a
250+ build that contains PR #501 . As of writing (2026-05-13) no ` 0.5.1 `
251+ release tag exists yet, so the options are:
252+
253+ ``` bash
254+ # Option A: install from upstream main (contains PR #501)
255+ pip install --upgrade --force-reinstall \
256+ git+https://github.com/netboxlabs/netbox-custom-objects.git@main
257+
258+ # Option B: wait for the 0.5.1 release tag and pin to it
259+ pip install --upgrade ' netbox-custom-objects>=0.5.1'
260+ ```
261+
262+ Then restart NetBox. The entire delete-bug class disappears regardless
263+ of this plugin's state — no plugin-side change required.
264+
265+ Several adjacent fixes also landed in upstream ` main ` post-0.5.0 and
266+ will ship with ` 0.5.1 ` : PR #504 (cross-COT FK fields after restart),
267+ PR #505 (stale through-model FK path_infos on COT regeneration), and
268+ PR #510 (self-referential FK isinstance check). Upgrading once closes
269+ the whole family.
270+
271+ #### Workarounds if you cannot upgrade yet
272+
273+ 1 . ** ` manage.py shell ` direct delete** (recommended for one-off rows).
274+ A freshly-spawned shell process initialises the model cache exactly
275+ once, so the class identity is consistent throughout the session and
276+ the collector's identity-check succeeds:
277+ ``` bash
278+ /opt/netbox/venv/bin/python /opt/netbox/netbox/manage.py shell << 'PY '
279+ from netbox_custom_objects.models import CustomObjectType
280+ cot = CustomObjectType.objects.get(slug="<your-slug>")
281+ cot.get_model().objects.filter(pk=<row-pk>).delete()
282+ PY
283+ ```
284+ 2. **Refresh the typed-tab list page** between Create and per-row Delete.
285+ This worked reliably for non-polymorphic fields on earlier versions
286+ and still often works on 0.5.0, but it is no longer guaranteed —
287+ polymorphic-MultiObject rows can drift the model cache mid-flow.
288+ 3. **Restart NetBox.** Clears `_model_cache` outright. Reliable but
289+ heavyweight; use when shell access isn't available.
290+
291+ #### Why polymorphic fields amplify the bug on 0.5.0
292+
293+ `netbox-custom-objects` 0.5.0 introduced `is_polymorphic=True` Object /
294+ MultiObject fields. Each polymorphic Object field adds a
295+ `GenericForeignKey` descriptor and each polymorphic MultiObject field
296+ adds a per-field through model. Django's collector traverses every
297+ related model when collecting deletion dependencies, so each extra
298+ related-model is another opportunity to hit a stale class generation in
299+ `CustomObjectType._model_cache`. Plugin 2.4.0's discovery code walks
300+ those same descriptors to find inbound links (the original goal of
301+ 2.4.0), which warms the cache enough that the upstream drift becomes
302+ deterministic rather than intermittent.
303+
304+ #### Root cause (for the curious)
305+
306+ Each Custom Object Type backs a dynamically-generated Django model
307+ (`Table<N>Model`), and the class registry can rebuild between requests
308+ (or during a request that touches `get_model(no_cache=True)`). Django's
309+ `Collector` then sees the queryset's model class on one side and a
310+ related-field descriptor's `.to` pointing at a *different copy of the
311+ same class name* on the other — its identity check raises `ValueError`.
312+ PR #501 fixes the symptom by overriding
313+ `CustomObjectDeleteView._get_dependent_objects` to filter through-table
314+ entries out of the collector's dependency walk before the identity check
315+ runs. This plugin does not override delete or model caching and cannot
316+ patch the bug from its own code.
317+
318+ #### Cosmetic follow-up on patched builds
319+
320+ On builds that already contain PR #501, the delete-success toast for
321+ some dynamic models renders as `"Deleted <Type> <Type> None"` — the
322+ patched view reads `str(obj)` *after* the row's deletion, so the
323+ dynamic model's primary field returns `None`. Models whose `__str__`
324+ captures the display value before delete are unaffected. This is a
325+ cosmetic, post-fix upstream issue; it does not affect the delete
326+ itself.
243327
244328## Support
245329
0 commit comments