Closes: #392 - Validate semantic version fields#455
Conversation
# Conflicts: # netbox_custom_objects/tests/test_schema_format.py
# Conflicts: # netbox_custom_objects/tests/test_schema_format.py
# Conflicts: # netbox_custom_objects/tests/test_models.py
|
No longer blocked. |
Resolve add/add and content conflicts:
- schema/{comparator,exporter}.py: use constants.TABLE_MODEL_RE (no local re import)
- schema/exporter.py: match removed_fields by slug (not name)
- schema/executor.py: raise RuntimeError instead of assert
- tests/schema/test_executor.py: include test_alter_field_type
- api/views.py: keep both ETagMixin shim and status import
- tests/test_api.py: keep both Pep440APIValidationTestCase and SchemaIdReadOnlyTest
- tests/test_models.py: keep feature's PluginConfig error-handling tests,
SemverValidationTestCase, and CrossCOTStubSearchIndexRegressionTestCase
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…omparator.py Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
_meta.get_field() triggers Django's lazy _relation_tree, which calls apps.get_models(), which re-enters our get_models() override, which calls get_model() for every COT — infinite recursion that eventually crashes with ContentType.DoesNotExist. Replace with a set built from _meta.local_fields and _meta.local_many_to_many, which are plain lists populated at model class-creation time and never trigger the relation tree. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
_after_model_generation iterated both active and tombstoned fields (_trashed_field_objects) and called model._meta.get_field(field_name) to check presence. For tombstoned fields absent from the model, get_field() cannot find the name in _forward_fields_map and falls through to fields_map, which lazily computes _relation_tree. _relation_tree calls apps.get_models(), re-entering our override, which calls get_model() for every COT -- including the one currently being generated (not yet cached) -- causing unbounded recursion. Replace the get_field() call with a present_fields dict built from model._meta.local_fields and model._meta.local_many_to_many, the same safe pattern used in register_custom_object_search_index. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two complementary fixes: 1. Add _app_ready flag to gate get_models() dynamic generation. Other apps' ready() calls (e.g. dcim.ready()) trigger Device._meta._relation_tree → apps.get_models() before our own ready() has run. At that point _model_cache is empty, so get_model() tries to regenerate every COT from scratch -- including ContentType DB lookups for MULTIOBJECT fields that may fail. get_models() now returns only static models until ready() completes and sets _app_ready = True. ready() then calls apps.clear_cache() so the next _relation_tree access recomputes with the full COT model set. 2. Catch ContentType.DoesNotExist in _fetch_and_generate_field_attrs. If a MULTIOBJECT field's related_object_type_id references a ContentType that no longer exists (stale data, manual cleanup, etc.), log a warning and skip the field rather than crashing the entire model generation. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…bject_type get_serializer_class iterated all DB fields without checking whether they were actually generated onto the model. Fields skipped during model generation (broken/null related_object_type_id) caused get_serializer_field() to crash on field.related_object_type.model_class() when related_object_type is None. Fix: build model_field_names from local_fields/local_many_to_many once and gate both loops in get_serializer_class against it, mirroring the same pattern used in _after_model_generation. Also improve the warning message in _fetch_and_generate_field_attrs to distinguish between a null FK (never set) vs a missing ContentType row. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… methods ObjectFieldType and MultiObjectFieldType call ContentType.objects.get() in get_model_field, get_form_field, get_filterform_field, and get_serializer_field. All callers already catch NotImplementedError and skip the field, but DoesNotExist (raised when related_object_type_id is NULL or the row was deleted) was not caught, crashing every view that iterated COT fields. Add _get_related_content_type() to the FieldType base class. It raises NotImplementedError for a null FK or missing ContentType row instead of letting DoesNotExist propagate. Replace all seven ContentType.objects.get(pk=field.related_object_type_id) calls in ObjectFieldType and MultiObjectFieldType with this helper so that every caller gets a consistent, handled skip. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Both ObjectFieldType and MultiObjectFieldType get_serializer_field access field.related_object_type.model_class() directly without first validating the FK. When related_object_type_id is NULL this crashes with AttributeError instead of the NotImplementedError that callers expect. Call _get_related_content_type() at the top of both methods to perform the validation before accessing the FK object. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…eration _fetch_and_generate_field_attrs only caught ContentType.DoesNotExist, but _get_related_content_type now raises NotImplementedError for null FKs too. Widen the except to catch both so model generation skips invalid fields gracefully regardless of which exception surfaces. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
These messages fire for test/stale data with null related_object_type_id. They are not actionable by operators and appeared multiple times at startup (model generation runs with skip_object_fields=True for stub models too). DEBUG keeps them available for diagnostics without cluttering production logs. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- models.py: ContentType.DoesNotExist in _fetch_and_generate_field_attrs was dead code — _get_related_content_type() converts it to NotImplementedError before it can escape get_model_field(). Simplify except clause to just NotImplementedError. - field_types.py: after_model_generation() and create_m2m_table() in MultiObjectFieldType still had raw ContentType.objects.get() calls that were missed during the centralisation refactor (they use 'instance' not 'field', making them invisible to find-and-replace). Replace both with self._get_related_content_type(instance) for consistency and safety. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ch index Tests that would have caught the three bugs fixed earlier this session: - register_custom_object_search_index() with skip_object_fields stub model - get_model() skips OBJECT field with NULL related_object_type_id - get_serializer_class() handles field absent from model (Meta.fields/attrs guard) - ObjectFieldType/MultiObjectFieldType.get_model_field() raises NotImplementedError for null FK Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
arthanson
left a comment
There was a problem hiding this comment.
need to fix the merge conflicts
The test exercises a method on CustomObjectType, not the reindex job or its triggers. SearchReindexTestCase is the wrong home for it. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Additional fixes bundled into this PRWhile integrating this branch with How the bugs entered
|
| Commit | What it fixes |
|---|---|
0c9ada6 |
Replace _meta.get_field() in register_custom_object_search_index() with local_fields / local_many_to_many set membership check |
9617203 |
Same replacement in _after_model_generation() for tombstoned fields in _trashed_field_objects |
409efe6 |
Add _app_ready flag to get_models(): skip all dynamic model generation until ready() has fully completed. Prevents dcim.ready() → Device._meta.get_fields() → _relation_tree → our get_models() from attempting COT model generation before the plugin is initialised |
052a0b1 |
Centralise null/missing related_object_type_id handling into FieldType._get_related_content_type(), which converts ContentType.DoesNotExist and null-FK into NotImplementedError. Add model_field_names guard in get_serializer_class() so Meta.fields and serializer attrs stay consistent (DRF raises on mismatch) |
f7f39f5 |
Replace all remaining raw ContentType.objects.get(pk=field.related_object_type_id) calls in field_types.py with self._get_related_content_type(field) so every caller gets a consistent NotImplementedError |
fd4c006 |
Add _get_related_content_type() guard at the top of both get_serializer_field() methods so a null/missing FK raises before field.related_object_type.model_class() is called |
ed1418a |
Widen the except clause in _fetch_and_generate_field_attrs() from ContentType.DoesNotExist to NotImplementedError to match what _get_related_content_type() now raises |
9cb440e |
Downgrade the "skipping field" log messages from WARNING to DEBUG; they were appearing twice at startup because get_model() was being called twice per COT on the stub path |
a2b3191 |
Remove now-dead ContentType.DoesNotExist from the except tuple (unreachable after centralisation); fix two raw ContentType.objects.get() calls in MultiObjectFieldType that used the instance parameter and were missed by the earlier search-and-replace |
bcf7f10 |
Regression tests: get_model() with null FK, get_serializer_class() with skipped field, register_custom_object_search_index() with stub model, ObjectFieldType/MultiObjectFieldType.get_model_field() raises NotImplementedError for null FK |
d48ca78 |
Move the search-index regression test from SearchReindexTestCase to CustomObjectTypeTestCase where it belongs |
- SchemaApplyView: enforce add+change permissions on CustomObjectType before calling apply_document(); any authenticated token-write user could previously trigger DDL operations without model-level permission - LinkedObjectsView: remove no_cache=True from get_model() — the cache exists for exactly this scenario (same COT referenced across multiple fields in one request) - models.py: move logger to module level instead of importing logging inside the except block on every skipped field Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…import - test_comparator.py: update import from netbox_custom_objects.exporter to netbox_custom_objects.schema.exporter (exporter was moved to the schema sub-package in an earlier PR) - views.py: remove redundant deferred import of CustomObjectType inside get_form_kwargs() — already imported at module level on line 28 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
SchemaApplyView now requires add+change permissions on CustomObjectType. Grant them via ObjectPermission in setUp() so all apply tests pass. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
arthanson
left a comment
There was a problem hiding this comment.
jsonschema is listed in pyproject.toml dependencies (hard requirement), but api/views.py wraps its import in a try/except and silently no-ops schema validation if it fails (_HAS_JSONSCHEMA = False).
If it's required, the ImportError path is dead code and the silent-pass fallback should be replaced with a startup error. If it's optional, it shouldn't be in dependencies. Either way, the OSError/json.JSONDecodeError case (missing/corrupt schema file) is a separate concern and should be handled explicitly regardless.
Merge feature (PRs #453/#454/#455) into 31-polymorphic-object-fields and fix all conflicts. Fix all items from the latest review (#4201168389): HIGH - views.py custom_save: set polymorphic GFK attributes on the instance before the first save() call, eliminating the double-save / double ObjectChange. MEDIUM - field_types.py create_polymorphic_m2m_table: accept schema_editor from caller instead of opening a nested with connection.schema_editor() context, which flushes deferred SQL prematurely on PostgreSQL. - models.py _original_is_polymorphic: use self.is_polymorphic instead of self.__dict__.get() to avoid False when field is deferred. - serializers.py PolymorphicObjectSerializerField: catch ValueError/TypeError on ContentType.objects.get(pk=...) for non-integer content_type_id input. - serializers.py get_serializer_class: derive _poly_obj_fields/_poly_m2m_fields from the already-evaluated model_fields queryset (no extra DB queries). - field_types.py MultiObjectFieldType.get_serializer_field: add allow_null=not field.required to the ListField for polymorphic multiobject fields so PATCH with null on non-required fields returns 200 not 400. Migration path - Rename 0009_polymorphic_object_fields to 0010_polymorphic_object_fields and update its dependency to 0009_alter_customobjecttype_version. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Closes: #392
Summary
_validate_semvervalidator (backed bypackaging.version.Version) to three version-string fields:CustomObjectType.version— the schema version of the COT itselfCustomObjectTypeField.deprecated_since— version in which the field was marked deprecatedCustomObjectTypeField.scheduled_removal— version in which the field is planned to be removedblank=True; the validator is a no-op for empty stringspackaging.version), which is already a project dependency and accepts standard semver strings (1.0.0,2.3.4, etc.) as well as Python-style pre/post-release suffixes (1.0.0a1,1.0.0.post1)0007) recording the field alterationsChanged files
models.pypackaging.version.{Version,InvalidVersion}; add_validate_semver; wire onto threeCharFielddefinitionsmigrations/0007_alter_customobjecttype_version_and_more.pyAlterFieldoperationstests/test_models.pySemverValidationTestCase— 9 tests covering blank (valid), valid PEP 440 strings, and invalid strings for all three fieldsPart of the portable schema format cluster