Skip to content

Fix single valued Dexterity UID references not settable - #115

Open
ramonski wants to merge 1 commit into
2.xfrom
fix/single-valued-dx-uidreference
Open

ramonski wants to merge 1 commit into
2.xfrom
fix/single-valued-dx-uidreference

Conversation

@ramonski

@ramonski ramonski commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

Setting a single valued UID reference on Dexterity content stopped working, and a second, older fault was hiding behind it. Both surface as a WrongType or WrongContainedType on a field the request never mentions, because the object is validated as a whole after the value is stored.

Description of the issue/feature this PR addresses

#106 taught the field manager to unwrap a single valued reference from its list to a bare UID. That is what an Archetypes UIDReferenceField needs: it refuses a list with "[...] is not supported".

A Dexterity UIDReferenceField is a different thing. It derives from zope.schema.List, and _validate runs the list validation before the length check:

def _validate(self, value):
    """Validator when called from form submission"""
    super(UIDReferenceField, self)._validate(value)
    # check if the fields accepts single values only
    if not self.multi_valued and len(value) > 1:
        raise ValueError("Single valued field accepts at most 1 value")

ZopeSchemaFieldManager._set validates before it stores, so a bare UID never reaches the field.

The second fault is older. A Dexterity type migrated from Archetypes keeps the old capitalised name as a BBB property, and a payload written against the old API uses it. get_field looked the name up exactly, so Department matched no schema field, and DexterityDataManager.set fell back to the BBB setter.

Some of those setters convert the value on the way in:

def setRetentionPeriod(self, value):
    default_period = default_retention_period()
    mutator = self.mutator("retention_period")
    mutator(self, dtime.to_timedelta(value, default=default_period))

Many hand it to the mutator exactly as it came:

def setDepartment(self, value):
    mutator = self.mutator("department")
    mutator(self, value)

Nineteen multi-word BBB setters are of the second kind on a field that needs its manager: UID references, durations, decimals and datagrid rows. A duration stays the mapping JSON carried, a UID stays unicode.

Current behavior before PR

Measured on AnalysisCategory.department (UIDReferenceField, multi_valued=False, required=True):

payload key schema field found stored type api.validate
department yes str {}
Department no unicode {'department': 'WrongContainedType'}

And Department.manager could not be set at all: the bare UID fails WrongType before it is stored.

Desired behavior after PR is merged

The unwrapping moves to the Archetypes field manager, behind a hook, so each implementation is handed the shape its own validator expects:

    def to_field_value(self, refs):
        """Shape the UIDs the way this field's own validator wants them.

        The two implementations disagree on a single valued field, and
        both validate before they store, so the value has to arrive in
        the shape each one expects.
        """
        return refs

ATUIDReferenceFieldManager overrides it to unwrap; the Dexterity manager keeps the list.

get_field tries the schema spellings after the exact name, and uses only a candidate that names a real field:

    candidates = [
        name,
        name[:1].lower() + name[1:],
        to_snake_case(name),
        name.lower(),
    ]

There are 140 such BBB properties on Dexterity content and 112 of them name a schema field. Lowering the first letter reaches 43 of those; the rest are snake case. A few keep neither convention, which is what the plain lower case form is for: a sample template calls its field samplepoint.

Verification

  • bin/test-senaite -s senaite.jsonapi: 41 tests, 0 failures, 0 errors.
  • The new uidreferences.rst covers a single valued Dexterity reference by schema name and by BBB name, a list of one, more than one (refused), and a single valued Archetypes reference. The payload is JSON, which is what makes the UIDs unicode.
  • Checked by reverting each fix on its own: the doctest fails both times, so it tests what it claims to.

I confirm I have tested the PR thoroughly and coded it according to PEP8 standards.

@ramonski
ramonski force-pushed the fix/single-valued-dx-uidreference branch from 2cfd010 to 21ab0f5 Compare October 7, 2026 05:36
@ramonski
ramonski requested a review from xispa October 7, 2026 07:24
@ramonski ramonski added Enhancement ✨ Improvement to existing functionality Bug 🐞 Something is broken and removed Enhancement ✨ Improvement to existing functionality labels Oct 7, 2026
Two separate faults, both of which end as a WrongContainedType or a
WrongType on a field the request never mentions.

#106 unwrapped a single valued reference from its list to a bare UID.
An Archetypes UIDReferenceField needs that, and refuses a list with
"[...] is not supported". A Dexterity UIDReferenceField derives from
zope.schema List and validates as one, so the bare UID fails before it
is ever stored, and setting e.g. a department's manager through the API
stopped working. The unwrapping moves to the Archetypes field manager,
through a to_field_value hook, so each side is handed the shape its own
validator expects.

The second fault is older and was hiding behind the first. A Dexterity
type migrated from Archetypes keeps the old capitalised name as a BBB
property, and a payload written against the old API uses it. get_field
looked the name up exactly, so the schema field was not found and the
data manager fell back to the BBB setter. Some of those setters convert
the value, and many hand it to the mutator as it came: a duration stays
the mapping JSON carried, a UID stays unicode.

The schema spellings are tried after the exact name, and only a
candidate naming a real field is used. There are 140 of these BBB
properties on Dexterity content, and 112 of them name a schema field.
Lowering the first letter reaches 43; the rest are snake case. A few
keep neither convention (a sample template calls its field
`samplepoint`), which is why the plain lower case form is tried too.

The new doctest covers both faults, and fails if either fix is
reverted.
@ramonski
ramonski force-pushed the fix/single-valued-dx-uidreference branch from 21ab0f5 to 759f4f6 Compare October 7, 2026 07:28
ramonski added a commit that referenced this pull request Oct 7, 2026
A name in a record that matched no field and no setter was dropped
with a line in the log, and the request answered that the object had
been created or updated. A caller asking for `Service` instead of
`services` was told that all was well and got back a profile with
nothing in it, which is how an afternoon goes missing.

It is refused now, and the message names the field the caller probably
meant:

    No field named 'Service' on AnalysisProfile. Did you mean 'services'?

The keys that address the object or steer the request rather than
naming a field (portal_type, parent_path, parent_uid, path, uid, id,
transition) are listed as such and skipped, as `id` already was.

This builds on #115. A name a Dexterity type kept from its Archetypes
days has to reach its field first; without that, every payload written
against the documented API, which uses those names throughout, would
now be refused.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Bug 🐞 Something is broken

Development

Successfully merging this pull request may close these issues.

1 participant