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 @@ -15,6 +15,7 @@ Changelog
applied. Uninstalling from the same panel removes the PAS plugin
and the per-user JWT signing secrets.

- #110 Fix /registry route failing on datetime registry values
- #108 Add introspection endpoints for types, schema and workflow
- #107 Validate only the submitted fields on update
- #109 Add a partition operation endpoint
Expand Down
26 changes: 25 additions & 1 deletion src/senaite/jsonapi/api/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
from Products.CMFPlone.interfaces.controlpanel import IMaintenanceSchema
from Products.CMFPlone.interfaces.controlpanel import ISecuritySchema
from Products.CMFPlone.interfaces.controlpanel import IUserGroupsSettingsSchema # noqa: E501
from senaite.core.api import dtime
from zope.component import getAdapter
from zope.schema import getFieldNames

Expand All @@ -46,6 +47,28 @@
}


def to_json_value(value):
"""Coerce a registry value into a JSON serializable form.

Registry records may hold dates or datetimes (and containers of them),
which the JSON encoder used by the route cannot serialize.

:param value: The raw registry value
:returns: A JSON serializable value
"""
if isinstance(value, (list, tuple)):
return [to_json_value(item) for item in value]
if isinstance(value, dict):
return {key: to_json_value(val) for key, val in value.items()}
# datetime and Zope DateTime
if dtime.is_dt(value) or dtime.is_DT(value):
return dtime.to_iso_format(value)
# pure date, which to_iso_format does not handle
if dtime.is_d(value):
return value.isoformat()
return value


def get_registry_records_by_keyword(keyword=None):
"""Return registry records whose name contains `keyword`.

Expand All @@ -55,7 +78,8 @@ def get_registry_records_by_keyword(keyword=None):
records = {}
for record in portal_reg.records:
if keyword is None or keyword.lower() in record.lower():
records[record] = bika_api.get_registry_record(record)
value = bika_api.get_registry_record(record)
records[record] = to_json_value(value)
return records


Expand Down
108 changes: 108 additions & 0 deletions src/senaite/jsonapi/tests/doctests/api_settings.rst
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,114 @@ filtered subset:
True


Non JSON serializable values are coerced
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Some registry records hold `datetime` values, which the JSON encoder used
by the route cannot serialize. Such values are coerced to ISO strings:

>>> from datetime import datetime
>>> from plone.registry import field
>>> from plone.registry.record import Record
>>> reg = ploneapi.portal.get_tool("portal_registry")
>>> reg.records["senaite.jsonapi.test_datetime"] = Record(
... field.Datetime(title=u"Test"),
... datetime(2014, 8, 14, 0, 0, 0, 3))
>>> transaction.commit()

>>> hits = api_settings.get_registry_records_by_keyword("test_datetime")
>>> hits["senaite.jsonapi.test_datetime"]
'2014-08-14T00:00:00.000003'

So the whole record set is now JSON serializable:

>>> ignored = json.dumps(
... api_settings.get_registry_records_by_keyword(None))

`to_json_value` does the coercion. Plain JSON values are returned as is:

>>> to_json_value = api_settings.to_json_value
>>> to_json_value("text")
'text'
>>> to_json_value(u"text")
u'text'
>>> to_json_value(1), to_json_value(1.5)
(1, 1.5)
>>> to_json_value(True), to_json_value(None)
(True, None)

A `datetime` is converted with `isoformat`, keeping its timezone if any:

>>> import pytz
>>> to_json_value(datetime(2014, 8, 14, 10, 30))
'2014-08-14T10:30:00'
>>> to_json_value(datetime(2014, 8, 14, 10, 30, tzinfo=pytz.utc))
'2014-08-14T10:30:00+00:00'

A `date` is converted with `isoformat` too:

>>> from datetime import date
>>> to_json_value(date(2014, 8, 14))
'2014-08-14'

A Zope `DateTime` is converted with `DateTime.ISO`, keeping its timezone:

>>> from DateTime import DateTime
>>> to_json_value(DateTime("2014/08/14 10:30:00 UTC"))
'2014-08-14T10:30:00+00:00'
>>> to_json_value(DateTime("2014/08/14 10:30:00 GMT+2"))
'2014-08-14T10:30:00+02:00'

Lists and tuples are converted item by item. Both come back as a list:

>>> to_json_value([date(2014, 8, 14), "text", 1])
['2014-08-14', 'text', 1]
>>> to_json_value((date(2014, 8, 14), "text"))
['2014-08-14', 'text']
>>> to_json_value([]), to_json_value(())
([], [])

Dicts keep their keys and have their values converted:

>>> to_json_value({"day": date(2014, 8, 14), "count": 1}) == {
... "day": "2014-08-14", "count": 1}
True
>>> to_json_value({})
{}

Nested containers are converted at every level:

>>> value = {"days": [date(2014, 8, 14), (datetime(2014, 8, 14, 10),)],
... "inner": {"when": DateTime("2014/08/14 10:30:00 UTC")}}
>>> to_json_value(value) == {
... "days": ["2014-08-14", ["2014-08-14T10:00:00"]],
... "inner": {"when": "2014-08-14T10:30:00+00:00"}}
True
>>> ignored = json.dumps(to_json_value(value))

The value passed in is not modified:

>>> value["days"][0]
datetime.date(2014, 8, 14)

Registry records of the other date-like and container field types are
coerced on the way out as well:

>>> reg.records["senaite.jsonapi.test_date"] = Record(
... field.Date(title=u"Test"), date(2014, 8, 14))
>>> reg.records["senaite.jsonapi.test_datetimes"] = Record(
... field.List(title=u"Test", value_type=field.Datetime()),
... [datetime(2014, 8, 14, 10, 30)])
>>> transaction.commit()

>>> hits = api_settings.get_registry_records_by_keyword(
... "senaite.jsonapi.test_")
>>> hits["senaite.jsonapi.test_date"]
'2014-08-14'
>>> hits["senaite.jsonapi.test_datetimes"]
['2014-08-14T10:30:00']


get_settings_by_keyword via the /settings route
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Expand Down
Loading