Fixes: #708 - Represent select/multiselect fields as {value, label} in REST and GraphQL - #718
Conversation
|
@claude /review and limit response to actionable issues. |
|
Claude finished @bctiemann's task in 1m 51s —— View job Review
The implementation is clean and closely mirrors core NetBox's own pattern. Two actionable issues: 1. Stale
class SelectFieldType(FieldType):
graphql_annotation = str # now bypassed by CHOICE_TYPES guard
class MultiSelectFieldType(FieldType):
graphql_annotation = List[str] # sameAfter this PR, 2. Null select value not tested in REST
Nothing else blocks this; the core logic, the |
|
Addressed both in 63405a8:
259/260 in the affected test classes (1 pre-existing skip, expected). |
Core's netbox.api.fields.ChoiceField coerces numeric- and boolean-looking
string input (e.g. "1", "true") to int/bool in to_internal_value() before
checking choice membership. That's safe for developer-defined enum choices
with non-string values, but this plugin's choice values are always the raw
strings an admin typed into a choice set, so a choice like "1" or "true"
would fail to validate against itself -- the API rejecting its own GET
output on write.
Add SafeChoiceField (api/serializers.py), a subclass of core's ChoiceField
that keeps the {value, label} read representation but drops the write-side
coercion, and use it in SelectFieldType/MultiSelectFieldType's
get_serializer_field(). Local imports are used to avoid a circular import
(serializers.py imports field_types at module level).
Verified empirically by round-tripping GET output through PUT/PATCH, per
jnovinger's repro in review. Adds regression coverage for numeric- and
boolean-looking choice values on both select and multiselect fields,
including that out-of-set values are still rejected.
Closes: #718 (comment)
|
Pushed c6bd376 addressing Jason's finding on the choice-field coercion bug (inline reply here). Summary: core's |
…ST and GraphQL
SelectFieldType/MultiSelectFieldType never wrapped their stored value in a
labeled representation, unlike core NetBox's own ChoiceField convention
(e.g. dcim.Site.status), so both APIs returned the raw stored string(s)
directly - REST because get_serializer_field() used a plain DRF ChoiceField,
GraphQL because the field was just annotated with the raw scalar type.
REST: SelectFieldType now returns netbox.api.fields.ChoiceField (core's own
{value, label}-on-read field, matching every other core ChoiceField exactly)
instead of DRF's plain ChoiceField. MultiSelectFieldType's ListField now uses
the same core ChoiceField as its child, so each item reads the same way.
Both still accept a bare value/list of values on write, unchanged.
GraphQL: added a small CustomObjectChoiceType (value, label) and a resolver
for SELECT/MULTISELECT fields, mirroring core's CustomField.
resolve_selection_value() and reusing this plugin's own
CustomObjectTypeField.get_choice_label() for the label lookup - the same
helper already backing the UI table column and get_display_value().
Updated test_scalar_fields_query, whose existing assertion on the raw
"status" value is now intentionally outdated by this fix.
- Remove the now-stale graphql_annotation = str / List[str] on
SelectFieldType/MultiSelectFieldType. GraphQL resolution for these two
types goes through graphql/types.py's CHOICE_TYPES resolver, bypassing
get_graphql_annotation() entirely - the same reason ObjectFieldType/
MultiObjectFieldType never set graphql_annotation either. Drops the now-
unused List import.
- Add null-value coverage to SelectMultiSelectValueLabelAPITest: an unset
select field reads back as None (not {"value": None, ...}), and an unset
multiselect field reads back as None too (the underlying ArrayField's own
default, unaffected by this fix - DRF's serializer short-circuits to None
before ever calling the field's to_representation()).
Core's netbox.api.fields.ChoiceField coerces numeric- and boolean-looking
string input (e.g. "1", "true") to int/bool in to_internal_value() before
checking choice membership. That's safe for developer-defined enum choices
with non-string values, but this plugin's choice values are always the raw
strings an admin typed into a choice set, so a choice like "1" or "true"
would fail to validate against itself -- the API rejecting its own GET
output on write.
Add SafeChoiceField (api/serializers.py), a subclass of core's ChoiceField
that keeps the {value, label} read representation but drops the write-side
coercion, and use it in SelectFieldType/MultiSelectFieldType's
get_serializer_field(). Local imports are used to avoid a circular import
(serializers.py imports field_types at module level).
Verified empirically by round-tripping GET output through PUT/PATCH, per
jnovinger's repro in review. Adds regression coverage for numeric- and
boolean-looking choice values on both select and multiselect fields,
including that out-of-set values are still rejected.
Closes: #718 (comment)
c6bd376 to
e58c6cb
Compare
Closes: #708
Summary
SelectFieldType/MultiSelectFieldTypenever wrapped their stored value in a labeled representation, unlike every core NetBoxChoiceField(e.g.dcim.Site.status) — both APIs returned the raw stored string(s) directly. One underlying gap, surfacing in both REST and GraphQL, as the issue itself notes.Note: this PR targets the
715-integer-decimal-min-max-validationbranch (itself stacked on700-required-fields-not-enforced/#714), notmain— it builds on theget_serializer_field()methods those introduce. Retarget tomainonce the earlier PRs in the stack merge.SelectFieldType.get_serializer_field()now returnsnetbox.api.fields.ChoiceField— core's own field that represents{value, label}on read and still accepts a bare value on write — instead of DRF's plainChoiceField.MultiSelectFieldType'sListFielduses the same coreChoiceFieldas its child, so each item in the list reads the same way.CustomObjectChoiceType(value,label) and a resolver forSELECT/MULTISELECTfields, mirroring core'sCustomField.resolve_selection_value()almost exactly, and reusing this plugin's ownCustomObjectTypeField.get_choice_label()— the same helper already backing the UI table column (SelectFieldType.get_table_column_field()) andget_display_value().test_scalar_fields_query, whose existing assertion on the raw"status"value is now intentionally outdated by this fix.Test plan
SelectMultiSelectValueLabelAPITest(new,test_api.py) — REST GET returns{value, label}forselectand a list of such dicts formultiselect; POST still accepts bare values on write.test_multiselect_field_query_returns_value_label_pairs(new,test_graphql.py) — GraphQL query returns{value, label}per item for a multiselect field.test_scalar_fields_query(updated) — GraphQLselectfield now queried/asserted as{value, label}.test_graphqlsuite (32 tests, including the schema-assembly smoke test) passes.test_api,test_graphql,test_field_types,test_views— 455 tests) — 4 pre-existing errors observed, matching the already-establishednetbox_branchingapp_label baseline, confirmed unrelated.ruff checkpasses on all changed files.