Skip to content
Open
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.

- #114 Support DX Decimal fields via a field manager
- #109 Add a partition operation endpoint
- #106 Fix single-valued UID reference fields not settable through the JSON API
- #112 Allow updating the Laboratory through the API
Expand Down
6 changes: 6 additions & 0 deletions src/senaite/jsonapi/configure.zcml
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,12 @@
factory=".fieldmanagers.DatetimeFieldManager"
/>

<!-- Adapter for DX Decimal Fields -->
<adapter
for="zope.schema.interfaces.IDecimal"
factory=".fieldmanagers.DecimalFieldManager"
/>

<!-- Adapter for DX Duration (Timedelta) Fields -->
<adapter
for="senaite.core.schema.interfaces.IDurationField"
Expand Down
14 changes: 10 additions & 4 deletions src/senaite/jsonapi/datamanagers.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,20 @@
from senaite.jsonapi import api
from senaite.jsonapi.interfaces import IDataManager
from senaite.jsonapi.interfaces import IFieldManager
from senaite.jsonapi.fieldmanagers import DecimalFieldManager
from senaite.jsonapi.fieldmanagers import DurationFieldManager
from senaite.jsonapi.fieldmanagers import UIDReferenceFieldMixin

# Field managers that normalize the incoming value (resolve UIDs, coerce
# a duration mapping to a timedelta, ...). For these the raw set<Name>
# mutator would store the value unconverted, so the data manager must go
# through the field manager instead of the setter.
NORMALIZING_FIELD_MANAGERS = (UIDReferenceFieldMixin, DurationFieldManager)
# a duration mapping to a timedelta, a number to a Decimal, ...). For
# these the raw set<Name> mutator would store the value unconverted, so
# the data manager must go through the field manager instead of the
# setter.
NORMALIZING_FIELD_MANAGERS = (
UIDReferenceFieldMixin,
DurationFieldManager,
DecimalFieldManager,
)


class BaseDataManager(object):
Expand Down
45 changes: 45 additions & 0 deletions src/senaite/jsonapi/fieldmanagers.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
from AccessControl import Unauthorized
from bika.lims import api as bika_api
from datetime import timedelta
from decimal import Decimal
from decimal import InvalidOperation
from DateTime import DateTime
from Products.Archetypes.utils import mapply
from senaite.core.schema.uidreferencefield import UIDReferenceField
Expand Down Expand Up @@ -202,6 +204,49 @@ def to_mapping(value):
}


class DecimalFieldManager(ZopeSchemaFieldManager):
"""Adapter to get/set DX Decimal field values.

The stored value is a `decimal.Decimal`, which JSON has no type for.
A price arrives as a string, a float or an integer and every one of
them fails the field's own validation, so a Decimal field could not
be written through the API at all. Accept all three on set and
serialize back to a string, which is the only JSON form that keeps
the exact value.
"""
interface.implements(IFieldManager)

def set(self, instance, value, **kw):
value = self.to_decimal(value)
self.field.validate(value)
return self.field.set(instance, value)

def json_data(self, instance, default=None):
value = self.get(instance)
if not isinstance(value, Decimal):
return default
return str(value)

@staticmethod
def to_decimal(value):
"""Coerce a JSON value to Decimal, leaving anything else alone.

A value that is not a number is handed on untouched so the field
reports it, rather than this adapter raising an error of its own
for a value the field would describe better.
"""
if value is None or isinstance(value, Decimal):
return value
if isinstance(value, bool):
return value
if isinstance(value, (six.string_types, int, float)):
try:
return Decimal(str(value).strip())
except (InvalidOperation, ValueError):
return value
return value


class RichTextFieldManager(ZopeSchemaFieldManager):
"""Adapter to get/set the value of Rich Text Fields
"""
Expand Down
24 changes: 24 additions & 0 deletions src/senaite/jsonapi/tests/doctests/create.rst
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,30 @@ directly; it accepts a `{days, hours, minutes, seconds}` mapping:
>>> sample_point.sampling_frequency
datetime.timedelta(7)

A DX Decimal field has no JSON type either. It takes the number as a
string, which is the only form that keeps the exact value, and also
accepts a plain number:

>>> data = {"portal_type": "AnalysisProfile",
... "parent_path": api.get_path(portal.setup.analysisprofiles),
... "title": "Basic Panel",
... "analysis_profile_price": u"24.00",
... "analysis_profile_vat": 7}
>>> profile = create_json(data)
>>> profile.analysis_profile_price
Decimal('24.00')
>>> profile.analysis_profile_vat
Decimal('7')

It reads back as a string:

>>> from senaite.jsonapi.interfaces import IFieldManager
>>> from senaite.jsonapi.api import get_field
>>> field = get_field(profile, "analysis_profile_price")
>>> IFieldManager(field).json_data(profile)
'24.00'



Creating a Sample
~~~~~~~~~~~~~~~~~
Expand Down
Loading