Fixes: #719 - Enforce object/multiobject required at the model layer - #720
Conversation
ObjectFieldType/MultiObjectFieldType.get_model_field() hardcoded blank=True unconditionally on their generated model field(s), so "required" was enforced only at the REST serializer layer for these two types -- unlike every scalar type (fixed in #700), which also enforces it at the model layer via full_clean(). Tie blank to field.required for the plain (non-polymorphic) case, mirroring #700's scalar-type fix: - ObjectFieldType: the direct ForeignKey branch and the LazyForeignKey branch (_make_lazy_cot_fk, shared by the self-referential and cross-COT cases) both now pass blank=not field.required. FK fields are validated by Django's clean_fields(), so full_clean() now correctly rejects an unset required object field, closing the gap for real. - MultiObjectFieldType: the CustomManyToManyField also now passes blank=not field.required. This has NO effect on full_clean() -- Django's clean_fields() only iterates _meta.fields, which explicitly excludes M2M fields (there's no way to validate M2M state before a row exists anyway, since through-rows need an existing pk on both sides). It does matter to the required-toggle pre-flight check in CustomObjectTypeField.clean(), though: that check queries existing data directly via values_list() rather than relying on per-field full_clean() validation, so it now correctly rejects toggling a multiobject field to required while an existing row has no related objects -- previously untested and unreachable, since blank was always True. Scoped to the plain (non-polymorphic) case only, per the issue: polymorphic object fields (GenericForeignKey) and polymorphic multiobject fields (PolymorphicM2MDescriptor) have no real, directly-queryable backing field to attach blank to, and the existing required-toggle pre-flight check already treats both as unaffected -- no parallel mechanism is designed for them here. Verified empirically: a plain object field's FK now raises ValidationError from full_clean() when unset and required; a plain multiobject field's required-toggle pre-flight check now correctly rejects an existing blank row (confirmed the M2M values_list() query resolves as expected), while full_clean() itself still cannot and does not check it. Adds RequiredFieldEnforcementTestCase coverage for both field types in test_field_types.py, and required-toggle pre-flight tests for plain object/multiobject in test_models.py (splitting the old "does not crash" test to cover only the polymorphic cases, which remain unaffected).
|
@claude /review and limit response to actionable issues. |
|
Claude finished @bctiemann's task in 4m 13s —— View job Review: #720 Enforce object/multiobject required at the model layerOverall: the 1. Stale comment in
|
1. models.py:2989-2991 had a stale comment claiming plain object/multiobject fields "hardcode blank=True regardless of required" -- true before this PR, no longer true after it. Reworded to say only the polymorphic cases remain excluded from the required-toggle pre-flight check. 2. Investigated the concern that values_list() across the M2M field might compile to an INNER JOIN, silently excluding blank rows from the required-toggle pre-flight check and making test_required_toggle_rejected_when_existing_multiobject_row_is_blank pass for the wrong reason. Confirmed empirically (via a throwaway script, not committed) that Django generates a LEFT OUTER JOIN here -- a blank row comes back as (None,), not omitted -- so the existing test already passes for the right reason. Added test_required_toggle_rejected_when_one_of_several_multiobject_rows_is_blank as permanent regression coverage: a blank row alongside a filled one must still be caught, which would fail under an INNER JOIN.
|
Thanks — pushed f1f15c9 addressing both.
|
No behavior change -- condenses comments and docstrings added for this PR down to their essential why, and removes inline #-issue references per project comment conventions.
Closes: #719
Summary
Follow-up to #700/#714, raised during review of that work (h/t @jnovinger):
ObjectFieldType/MultiObjectFieldType.get_model_field()hardcodedblank=Trueunconditionally on their generated model field(s), sorequiredwas enforced only at the REST serializer layer for these two types -- every scalar type (fixed in #700) also enforces it at the model layer viafull_clean().ObjectFieldType: both the directForeignKeybranch and theLazyForeignKeybranch (_make_lazy_cot_fk, shared by the self-referential and cross-COT cases) now passblank=not field.required. FK fields are validated by Django'sclean_fields(), sofull_clean()now genuinely rejects an unset required object field.MultiObjectFieldType: theCustomManyToManyFieldalso now passesblank=not field.required. Important finding from investigating this: this has no effect onfull_clean()-- Django'sclean_fields()only iterates_meta.fields, which explicitly excludes M2M fields (confirmed against Django's ownOptions.fieldssource). There's no way to validate M2M state before a row exists anyway, since through-rows need an existing pk on both sides. It does matter to the required-toggle pre-flight check inCustomObjectTypeField.clean(), though: that check queries existing data directly viavalues_list()rather than relying on per-fieldfull_clean()validation, so it now correctly rejects toggling a multiobject field to required while an existing row has no related objects.Scoped to the plain (non-polymorphic) case only, per the issue: polymorphic object fields (
GenericForeignKey) and polymorphic multiobject fields (PolymorphicM2MDescriptor) have no real, directly-queryable backing field to attachblankto, and the existing required-toggle pre-flight check already treats both as unaffected -- no parallel mechanism is designed for them here.Test plan
RequiredFieldEnforcementTestCase.test_object_field_blank_matches_required/test_multiobject_field_blank_matches_required_but_full_clean_cannot_check_it(new,test_field_types.py) -- confirmsblankfollowsrequiredfor both types, and thatfull_clean()enforces it for object but (correctly, structurally) cannot for multiobject.test_models.py: split the old "does not crash" relationship-field test to cover only the polymorphic cases (which remain unaffected); addedtest_required_toggle_rejected_when_existing_object_row_is_blank/_allowed_when_no_blank_object_rowsand the multiobject equivalents, confirming the required-toggle pre-flight check now correctly handles both plain relationship types.req_sites(M2M) is absent from_meta.fieldsand present only in_meta.many_to_many, confirming whyfull_clean()structurally cannot check it.test_models,test_field_types,test_api,test_forms,test_views,test_graphql) -- 7 pre-existing failures observed (6 delete-path tests hitting the localnetbox_branchingapp_label gap, 1LOGIN_REQUIREDlocal-config artifact), all confirmed unrelated to this change.ruff checkpasses on all changed files.