Skip to content

Commit dd2c845

Browse files
authored
Fix inherited schema fields missing for multi-schema Dexterity types (#89)
* Support for multi-schema DX types * Added regression doctest * changelog
1 parent 0139af6 commit dd2c845

4 files changed

Lines changed: 114 additions & 14 deletions

File tree

docs/changelog.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ Changelog
44
2.7.0 (unreleased)
55
------------------
66

7+
- #89 Fix inherited schema fields missing for multi-schema Dexterity types
78
- #87 Inject additional analyses fields
89
- #85 Allow field projection
910
- #81 Support second-level precision on searches against DateIndex

src/senaite/jsonapi/api.py

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -749,15 +749,8 @@ def get_fields(brain_or_object):
749749
# The portal object has no schema
750750
if is_root(obj):
751751
return {}
752-
schema = get_schema(obj)
753-
if is_dexterity_content(obj):
754-
names = schema.names()
755-
fields = map(lambda name: schema.get(name), names)
756-
schema_fields = dict(zip(names, fields))
757-
# update with behavior fields
758-
schema_fields.update(get_behaviors(obj))
759-
return schema_fields
760-
return dict(zip(schema.keys(), schema.fields()))
752+
# rely on core's api
753+
return api.get_fields(obj)
761754

762755

763756
def get_field(brain_or_object, name, default=None):

src/senaite/jsonapi/dataproviders.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -207,9 +207,8 @@ def __init__(self, context):
207207
super(DexterityDataProvider, self).__init__(context)
208208

209209
# get the behavior and schema fields from the data manager
210-
schema = api.get_schema(context)
211-
behaviors = api.get_behaviors(context)
212-
self.keys = schema.names() + behaviors.keys()
210+
fields = api.get_fields(context)
211+
self.keys = fields.keys()
213212

214213

215214
class ATDataProvider(Base):
@@ -222,8 +221,8 @@ def __init__(self, context):
222221
super(ATDataProvider, self).__init__(context)
223222

224223
# get the schema fields from the data manager
225-
schema = api.get_schema(context)
226-
self.keys = schema.keys()
224+
fields = api.get_fields(context)
225+
self.keys = fields.keys()
227226

228227

229228
class AnalysisDataProvider(Base):
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
DEXTERITY INHERITED FIELDS
2+
--------------------------
3+
4+
Regression test for multi-schema Dexterity types.
5+
6+
When the searched object is a Dexterity (DX) type whose schema interface
7+
inherits from another schema interface, the fields declared by the *parent*
8+
schema must be returned too. The previous implementation relied on
9+
``schema.names()``, which only returns the fields declared directly on the
10+
interface (inherited fields require ``names(all=True)``), so inherited fields
11+
were silently dropped from the API response.
12+
13+
``senaite.core.content.supplier.Supplier`` is a good example of a multi-schema
14+
DX type: its schema ``ISupplierSchema`` inherits from ``IOrganizationSchema``.
15+
16+
Running this test from the buildout directory:
17+
18+
bin/test test_doctests -t dexterity_inherited_fields
19+
20+
21+
Test Setup
22+
~~~~~~~~~~
23+
24+
Needed Imports:
25+
26+
>>> import json
27+
>>> import transaction
28+
>>> from plone.app.testing import setRoles
29+
>>> from plone.app.testing import TEST_USER_ID
30+
31+
>>> from bika.lims import api
32+
>>> from senaite.jsonapi import api as japi
33+
34+
Functional Helpers:
35+
36+
>>> def get(url):
37+
... browser.open("{}/{}".format(api_url, url))
38+
... return browser.contents
39+
40+
Variables:
41+
42+
>>> portal = self.portal
43+
>>> setup = portal.setup
44+
>>> portal_url = portal.absolute_url()
45+
>>> api_url = "{}/@@API/senaite/v1".format(portal_url)
46+
>>> browser = self.getBrowser()
47+
>>> setRoles(portal, TEST_USER_ID, ["LabManager", "Manager"])
48+
>>> transaction.commit()
49+
50+
Create a Supplier (DX type with an inherited schema):
51+
52+
>>> supplier = api.create(setup.suppliers, "Supplier", Name="Naralabs")
53+
>>> uid = api.get_uid(supplier)
54+
>>> transaction.commit()
55+
56+
57+
The DX type is genuinely multi-schema
58+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
59+
60+
The schema interface inherits from a parent schema interface:
61+
62+
>>> from senaite.core.content.supplier import ISupplierSchema
63+
>>> from senaite.core.content.organization import IOrganizationSchema
64+
>>> IOrganizationSchema in ISupplierSchema.__bases__
65+
True
66+
67+
``tax_number`` is declared by the *parent* ``IOrganizationSchema``, while
68+
``lab_account_number`` is declared *directly* on ``ISupplierSchema``:
69+
70+
>>> "tax_number" in IOrganizationSchema.names()
71+
True
72+
>>> "tax_number" in ISupplierSchema.names()
73+
False
74+
>>> "lab_account_number" in ISupplierSchema.names()
75+
True
76+
77+
78+
Inherited fields are returned by get_fields
79+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
80+
81+
The fields mapping must contain both the directly declared field and the
82+
inherited one:
83+
84+
>>> fields = japi.get_fields(supplier)
85+
>>> "lab_account_number" in fields
86+
True
87+
>>> "tax_number" in fields
88+
True
89+
90+
``get_field`` resolves the inherited field too (it previously returned the
91+
default because the field was missing from the mapping):
92+
93+
>>> japi.get_field(supplier, "tax_number") is not None
94+
True
95+
96+
97+
Inherited fields are present in the API response
98+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
99+
100+
End to end, fetching the object exposes the inherited field as a key:
101+
102+
>>> response = get(uid)
103+
>>> data = json.loads(response)
104+
>>> "lab_account_number" in data
105+
True
106+
>>> "tax_number" in data
107+
True

0 commit comments

Comments
 (0)