Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ Changelog
2.7.0 (unreleased)
------------------

- #89 Fix inherited schema fields missing for multi-schema Dexterity types
- #87 Inject additional analyses fields
- #85 Allow field projection
- #81 Support second-level precision on searches against DateIndex
Expand Down
11 changes: 2 additions & 9 deletions src/senaite/jsonapi/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -749,15 +749,8 @@ def get_fields(brain_or_object):
# The portal object has no schema
if is_root(obj):
return {}
schema = get_schema(obj)
if is_dexterity_content(obj):
names = schema.names()
fields = map(lambda name: schema.get(name), names)
schema_fields = dict(zip(names, fields))
# update with behavior fields
schema_fields.update(get_behaviors(obj))
return schema_fields
return dict(zip(schema.keys(), schema.fields()))
# rely on core's api
return api.get_fields(obj)


def get_field(brain_or_object, name, default=None):
Expand Down
9 changes: 4 additions & 5 deletions src/senaite/jsonapi/dataproviders.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,9 +207,8 @@ def __init__(self, context):
super(DexterityDataProvider, self).__init__(context)

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


class ATDataProvider(Base):
Expand All @@ -222,8 +221,8 @@ def __init__(self, context):
super(ATDataProvider, self).__init__(context)

# get the schema fields from the data manager
schema = api.get_schema(context)
self.keys = schema.keys()
fields = api.get_fields(context)
self.keys = fields.keys()


class AnalysisDataProvider(Base):
Expand Down
107 changes: 107 additions & 0 deletions src/senaite/jsonapi/tests/doctests/dexterity_inherited_fields.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
DEXTERITY INHERITED FIELDS
--------------------------

Regression test for multi-schema Dexterity types.

When the searched object is a Dexterity (DX) type whose schema interface
inherits from another schema interface, the fields declared by the *parent*
schema must be returned too. The previous implementation relied on
``schema.names()``, which only returns the fields declared directly on the
interface (inherited fields require ``names(all=True)``), so inherited fields
were silently dropped from the API response.

``senaite.core.content.supplier.Supplier`` is a good example of a multi-schema
DX type: its schema ``ISupplierSchema`` inherits from ``IOrganizationSchema``.

Running this test from the buildout directory:

bin/test test_doctests -t dexterity_inherited_fields


Test Setup
~~~~~~~~~~

Needed Imports:

>>> import json
>>> import transaction
>>> from plone.app.testing import setRoles
>>> from plone.app.testing import TEST_USER_ID

>>> from bika.lims import api
>>> from senaite.jsonapi import api as japi

Functional Helpers:

>>> def get(url):
... browser.open("{}/{}".format(api_url, url))
... return browser.contents

Variables:

>>> portal = self.portal
>>> setup = portal.setup
>>> portal_url = portal.absolute_url()
>>> api_url = "{}/@@API/senaite/v1".format(portal_url)
>>> browser = self.getBrowser()
>>> setRoles(portal, TEST_USER_ID, ["LabManager", "Manager"])
>>> transaction.commit()

Create a Supplier (DX type with an inherited schema):

>>> supplier = api.create(setup.suppliers, "Supplier", Name="Naralabs")
>>> uid = api.get_uid(supplier)
>>> transaction.commit()


The DX type is genuinely multi-schema
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

The schema interface inherits from a parent schema interface:

>>> from senaite.core.content.supplier import ISupplierSchema
>>> from senaite.core.content.organization import IOrganizationSchema
>>> IOrganizationSchema in ISupplierSchema.__bases__
True

``tax_number`` is declared by the *parent* ``IOrganizationSchema``, while
``lab_account_number`` is declared *directly* on ``ISupplierSchema``:

>>> "tax_number" in IOrganizationSchema.names()
True
>>> "tax_number" in ISupplierSchema.names()
False
>>> "lab_account_number" in ISupplierSchema.names()
True


Inherited fields are returned by get_fields
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

The fields mapping must contain both the directly declared field and the
inherited one:

>>> fields = japi.get_fields(supplier)
>>> "lab_account_number" in fields
True
>>> "tax_number" in fields
True

``get_field`` resolves the inherited field too (it previously returned the
default because the field was missing from the mapping):

>>> japi.get_field(supplier, "tax_number") is not None
True


Inherited fields are present in the API response
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

End to end, fetching the object exposes the inherited field as a key:

>>> response = get(uid)
>>> data = json.loads(response)
>>> "lab_account_number" in data
True
>>> "tax_number" in data
True
Loading