Repository navigation
Conversation
A polymorphic Object field is a generic foreign key, which has no database FK, so deleting the object it pointed at left a dangling reference whatever its on_delete_behavior said. A pre_delete receiver now does what the database does for a non-polymorphic field: protect refuses the deletion with ProtectedError, cascade deletes the referencing custom objects, and set null clears the reference, logging the change as core does for a nullable FK. Core's pre_delete handler now runs after it, so the changes it makes are logged before the deletion that caused them, as Django orders a real FK's dependents. A branch revert depends on that order. The field form now offers the setting for polymorphic Object fields, and clean() no longer resets it to set null. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@claude /review and report only on actionable issues. |
|
Claude finished @bctiemann's task in 5m 44s —— View job PR Review: Fixes #902 — Apply on_delete_behavior to polymorphic Object fields
The core logic is sound: the 1.
|
ProtectedError raised from pre_delete came from inside the collector's transaction, so a caller catching it inside its own transaction was left unable to query. NetBox 4.5's delete views and REST API don't open one of their own, and the tests do. Check protect in the collector's delete() instead, before any signal or transaction, as Django refuses a deletion a real FK protects. NetBox's CustomCollector opens its transaction in its own delete(), so wrap that too where it exists. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Before mergingTwo follow-ups depend on other PRs:
Also check that CI is green at the head you merge. The v4.5.2 jobs are the ones that caught the protect transaction issue. |
apply_polymorphic_on_delete() ran on every deletion in NetBox and queried for polymorphic fields each time. The collector wrapper already sees every object a deletion removes, so polymorphic_on_delete() now looks up the fields for all their types in one query, checks protect, and hands the result to the pre_delete handler for the rest of the deletion. Also say where _netbox_private comes from. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thanks. Taking these in turn:
Note that this review ran on the first commit. Protect has since moved out of the |
Fixes: #902
A polymorphic Object field is a generic foreign key: a content type column and an object ID column, with no database FK. So
on_delete_behaviordid nothing for it. Deleting the referenced object always succeeded and left a dangling reference, whatever the field said. That's true even forset_null: the object ID and content type were never cleared.Changes
Protect (
polymorphic_on_delete()inmodels.py): the deletion collector'sdelete()is wrapped. Before anything else, it looks up in one query the polymorphic fields that may point at any type being deleted. It then checks the objects about to be deleted, with one query per protecting field. If a polymorphic field set to protect points at one of them, it raisesProtectedErrorlisting the referencing custom objects. Custom objects deleted in the same operation, for example by a real FK's cascade, don't count. This is the same point where Django refuses a deletion that a real FK protects: before any signal is sent or the deletion's transaction starts. So nothing has been logged or queued, and a caller's own transaction stays usable. NetBox'sCustomCollectoropens its transaction in its owndelete()on current NetBox, so the check wraps that too, and runs once per collector. The plugin already patches the collector, ininstall_collector_patch(). NetBox shows its usual "Unable to delete" message, and the REST API returns 409.Cascade and set null (
apply_polymorphic_on_delete(), apre_deletereceiver): uses the fields thatpolymorphic_on_delete()looked up for the deletion, so a delete of many objects adds one field lookup, not one per object. It finds the custom objects whose polymorphic field points at the object being deleted, then:Both skip migrations, auto-created through models and NetBox's private models (such as
CablePath).Receiver order (
__init__.py):ready()reconnects core'shandle_deleted_objectso it runs after the new receiver. What a cascade deletes or set null clears is then logged before the deletion that caused it. That's the order Django's collector logs a real FK's dependents in. A branch revert relies on it: it undoes changes newest first, and restoring a custom object's generic FK fails if its target hasn't been restored yet. The plugin already reconnects this handler whenever a type is deleted, so core's handler already ended up last after any type deletion.Form and model: the field form now offers
on_delete_behaviorfor polymorphic Object fields.clean()no longer resets it toset_nullfor them; it still does for non-Object fields. The REST API and schema apply already accepted it.Docs:
field-attributes.mdsays the setting applies to polymorphic Object fields too.Existing polymorphic fields saved with
protectorcascadethrough the REST API or schema apply will start enforcing it. Fields created in the UI were always reset toset_null.A protect reached only through a cascade, such as a custom object that is itself protected and is deleted by another field's cascade, is refused by the nested deletion, inside the outer one's transaction. Callers that catch the error outside their transaction, as NetBox's views and REST API do, aren't affected.
Polymorphic multi-object fields are unchanged: their stale through rows after a target is deleted are a separate, documented gap.
Testing
New module
tests/test_polymorphic_on_delete.py(17 tests):QuerySet.delete()is handled;clean()keeps the setting.Against unmodified
feature, 13 of the original 15 fail. The two that pass are the controls: the unreferenced delete, and the other type with the same pk. Without the receiver reorder, the two log-order tests fail.test_forms.py's visibility test now expects the setting on polymorphic Object fields.Both full suites pass, each with the 17 new tests: 1,768 tests without netbox-branching and 1,862 with it (6 skipped), 1 expected failure (#892) in each.
ReferencedObjectDeletionTestintest_polymorphic_fields.pystill passes, but its docstring says deleting a site leaves a stale object ID. #953 is moving that file. I'll correct the docstring there once #953 merges, to avoid a conflict git couldn't resolve.🤖 Generated with Claude Code