Skip to content

Commit 0139af6

Browse files
ramonskixispa
andauthored
Inject additional analyses fields (#87)
* Allow to select fields * Added doctest * Changelog updated * Additional Analyses Fields * changelog updated * Added doctest --------- Co-authored-by: Jordi Puiggené <jp@naralabs.com>
1 parent 32e6a2d commit 0139af6

4 files changed

Lines changed: 296 additions & 0 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+
- #87 Inject additional analyses fields
78
- #85 Allow field projection
89
- #81 Support second-level precision on searches against DateIndex
910
- #83 Fetch multiple items by UID

src/senaite/jsonapi/configure.zcml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,12 @@
5353
factory=".dataproviders.DexterityDataProvider"
5454
/>
5555

56+
<!-- Data provider for Analysis content types (computed fields) -->
57+
<adapter
58+
name="senaite.jsonapi.dataproviders.AnalysisDataProvider"
59+
factory=".dataproviders.AnalysisDataProvider"
60+
/>
61+
5662

5763
<!-- DATA MANAGERS
5864
Context level interface to get and set values (by name) and get a JSON compatible

src/senaite/jsonapi/dataproviders.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
from AccessControl import Unauthorized
2222
from Acquisition import aq_base
23+
from bika.lims.interfaces import IAnalysis
2324
from plone.dexterity.interfaces import IDexterityContent
2425
from Products.Archetypes.interfaces import IBaseObject
2526
from Products.CMFCore.interfaces import ISiteRoot
@@ -225,6 +226,38 @@ def __init__(self, context):
225226
self.keys = schema.keys()
226227

227228

229+
class AnalysisDataProvider(Base):
230+
"""Data provider for Analysis content types.
231+
232+
Supplements the standard ATDataProvider with computed fields that are
233+
implemented as methods rather than AT schema fields and therefore not
234+
picked up automatically.
235+
"""
236+
interface.implements(IInfo)
237+
component.adapts(IAnalysis)
238+
239+
def __init__(self, context):
240+
super(AnalysisDataProvider, self).__init__(context)
241+
# No schema keys – this provider only adds computed fields via
242+
# the attributes mapping below.
243+
self.keys = []
244+
self.attributes = {}
245+
246+
def to_dict(self):
247+
"""Return computed analysis fields."""
248+
out = {}
249+
250+
get_formatted = getattr(self.context, "getFormattedResult", None)
251+
if callable(get_formatted):
252+
out["getFormattedResult"] = get_formatted(html=False)
253+
254+
is_retest = getattr(self.context, "isRetest", None)
255+
if callable(is_retest):
256+
out["isRetest"] = is_retest()
257+
258+
return out
259+
260+
228261
class SiteRootDataProvider(Base):
229262
""" Site Root Adapter
230263
"""
Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
ANALYSIS DATA PROVIDER
2+
----------------------
3+
4+
Running this test from the buildout directory:
5+
6+
bin/test test_doctests -t analysis_data_provider
7+
8+
9+
Test Setup
10+
~~~~~~~~~~
11+
12+
Needed Imports:
13+
14+
>>> import json
15+
>>> import transaction
16+
>>> import urllib
17+
>>> from DateTime import DateTime
18+
>>> from plone.app.testing import setRoles
19+
>>> from plone.app.testing import TEST_USER_ID
20+
>>> from bika.lims import api
21+
22+
Functional Helpers:
23+
24+
>>> def get(url):
25+
... browser.open("{}/{}".format(api_url, url))
26+
... return browser.contents
27+
28+
>>> def post(url, data):
29+
... url = "{}/{}".format(api_url, url)
30+
... browser.post(url, urllib.urlencode(data, doseq=True))
31+
... return browser.contents
32+
33+
>>> def create(data):
34+
... response = post("create", data)
35+
... assert("items" in response)
36+
... response = json.loads(response)
37+
... items = response.get("items")
38+
... assert(len(items)==1)
39+
... item = response.get("items")[0]
40+
... assert("uid" in item)
41+
... return api.get_object(item["uid"])
42+
43+
Variables:
44+
45+
>>> portal = self.portal
46+
>>> portal_url = portal.absolute_url()
47+
>>> api_url = "{}/@@API/senaite/v1".format(portal_url)
48+
>>> setup = api.get_setup()
49+
>>> browser = self.getBrowser()
50+
>>> setRoles(portal, TEST_USER_ID, ["LabManager", "Manager"])
51+
>>> transaction.commit()
52+
53+
54+
Create test data
55+
~~~~~~~~~~~~~~~~
56+
57+
Create the required setup objects:
58+
59+
>>> client = create({
60+
... "portal_type": "Client",
61+
... "parent_path": api.get_path(portal.clients),
62+
... "title": "Test Client",
63+
... "ClientID": "TC"})
64+
65+
>>> contact = create({
66+
... "portal_type": "Contact",
67+
... "parent_path": api.get_path(client),
68+
... "Firstname": "Jane",
69+
... "Surname": "Doe"})
70+
71+
>>> sample_type = create({
72+
... "portal_type": "SampleType",
73+
... "parent_path": api.get_path(portal.setup.sampletypes),
74+
... "title": "Water",
75+
... "MinimumVolume": "100 ml",
76+
... "Prefix": "WA"})
77+
78+
>>> lab_contact = create({
79+
... "portal_type": "LabContact",
80+
... "parent_path": api.get_path(setup.bika_labcontacts),
81+
... "Firstname": "Lab",
82+
... "Surname": "Manager"})
83+
84+
>>> department = create({
85+
... "portal_type": "Department",
86+
... "DepartmentID": "CH",
87+
... "parent_path": api.get_path(portal.setup.departments),
88+
... "title": "Chemistry",
89+
... "Manager": api.get_uid(lab_contact)})
90+
91+
>>> category = create({
92+
... "portal_type": "AnalysisCategory",
93+
... "parent_path": api.get_path(portal.setup.analysiscategories),
94+
... "title": "Metals",
95+
... "Department": api.get_uid(department)})
96+
97+
>>> service = create({
98+
... "portal_type": "AnalysisService",
99+
... "parent_path": api.get_path(setup.bika_analysisservices),
100+
... "title": "Calcium",
101+
... "Keyword": "Ca",
102+
... "Price": 10,
103+
... "Category": api.get_uid(category)})
104+
105+
Create a Sample with one analysis:
106+
107+
>>> sample = create({
108+
... "portal_type": "AnalysisRequest",
109+
... "parent_uid": api.get_uid(client),
110+
... "Contact": api.get_uid(contact),
111+
... "DateSampled": DateTime().ISO8601(),
112+
... "SampleType": api.get_uid(sample_type),
113+
... "Analyses": [api.get_uid(service)]})
114+
115+
>>> analyses = sample.getAnalyses(full_objects=True)
116+
>>> len(analyses)
117+
1
118+
119+
>>> analysis = analyses[0]
120+
>>> analysis_uid = api.get_uid(analysis)
121+
122+
123+
Analysis fields via the API
124+
~~~~~~~~~~~~~~~~~~~~~~~~~~~
125+
126+
When fetching an analysis with ``complete=1``, the response includes the
127+
computed fields ``getFormattedResult`` and ``isRetest`` injected by the
128+
``AnalysisDataProvider``:
129+
130+
>>> response = get("analysis/{}?complete=1".format(analysis_uid))
131+
>>> data = json.loads(response)
132+
>>> items = data.get("items")
133+
>>> len(items)
134+
1
135+
136+
>>> item = items[0]
137+
138+
The ``getFormattedResult`` field is present. Since no result has been
139+
submitted yet, it returns an empty string:
140+
141+
>>> "getFormattedResult" in item
142+
True
143+
>>> item["getFormattedResult"]
144+
u''
145+
146+
The ``isRetest`` field is present and ``False`` for a regular analysis:
147+
148+
>>> "isRetest" in item
149+
True
150+
>>> item["isRetest"]
151+
False
152+
153+
154+
Formatted result after submitting a value
155+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
156+
157+
Receive the sample and submit a result for the analysis:
158+
159+
>>> from bika.lims.workflow import doActionFor as do_action_for
160+
>>> transitioned = do_action_for(sample, "receive")
161+
>>> analysis.setResult("42")
162+
>>> transaction.commit()
163+
164+
The formatted result now reflects the submitted value:
165+
166+
>>> response = get("analysis/{}?complete=1".format(analysis_uid))
167+
>>> data = json.loads(response)
168+
>>> item = data["items"][0]
169+
>>> item["getFormattedResult"]
170+
u'42'
171+
>>> item["isRetest"]
172+
False
173+
174+
175+
Formatted result with ResultOptions
176+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
177+
178+
Create a service with predefined result options (select type):
179+
180+
>>> service_ro = create({
181+
... "portal_type": "AnalysisService",
182+
... "parent_path": api.get_path(setup.bika_analysisservices),
183+
... "title": "Color",
184+
... "Keyword": "Color",
185+
... "Price": 5,
186+
... "Category": api.get_uid(category)})
187+
188+
>>> options = [
189+
... {"ResultValue": "0", "ResultText": "Colorless"},
190+
... {"ResultValue": "1", "ResultText": "Yellow"},
191+
... {"ResultValue": "2", "ResultText": "Brown"},
192+
... ]
193+
>>> service_ro.setResultOptions(options)
194+
>>> service_ro.setResultType("select")
195+
>>> transaction.commit()
196+
197+
Create a sample with this service:
198+
199+
>>> sample2 = create({
200+
... "portal_type": "AnalysisRequest",
201+
... "parent_uid": api.get_uid(client),
202+
... "Contact": api.get_uid(contact),
203+
... "DateSampled": DateTime().ISO8601(),
204+
... "SampleType": api.get_uid(sample_type),
205+
... "Analyses": [api.get_uid(service_ro)]})
206+
207+
>>> analyses2 = sample2.getAnalyses(full_objects=True)
208+
>>> an_color = analyses2[0]
209+
>>> an_color_uid = api.get_uid(an_color)
210+
211+
Receive the sample and set a result using one of the option values:
212+
213+
>>> transitioned = do_action_for(sample2, "receive")
214+
>>> an_color.setResult("1")
215+
>>> transaction.commit()
216+
217+
The formatted result returns the display text, not the raw value:
218+
219+
>>> response = get("analysis/{}?complete=1".format(an_color_uid))
220+
>>> data = json.loads(response)
221+
>>> item = data["items"][0]
222+
>>> item["getFormattedResult"]
223+
u'Yellow'
224+
>>> item["isRetest"]
225+
False
226+
227+
228+
Retest analysis
229+
~~~~~~~~~~~~~~~
230+
231+
Submit the first analysis and then retract it to create a retest:
232+
233+
>>> transitioned = do_action_for(analysis, "submit")
234+
>>> transitioned = do_action_for(analysis, "retract")
235+
>>> transaction.commit()
236+
237+
The retracted analysis now has a retest. Fetch the retest:
238+
239+
>>> retest = analysis.getRetest()
240+
>>> retest_uid = api.get_uid(retest)
241+
242+
The retest is flagged as such via the API:
243+
244+
>>> response = get("analysis/{}?complete=1".format(retest_uid))
245+
>>> data = json.loads(response)
246+
>>> item = data["items"][0]
247+
>>> item["isRetest"]
248+
True
249+
250+
The original retracted analysis is not a retest:
251+
252+
>>> response = get("analysis/{}?complete=1".format(analysis_uid))
253+
>>> data = json.loads(response)
254+
>>> item = data["items"][0]
255+
>>> item["isRetest"]
256+
False

0 commit comments

Comments
 (0)