Skip to content

Commit 4496cfa

Browse files
ramonskiclaude
andauthored
Fetch multiple items by UID (#83)
* Fetch multiple results by UID * Fix UID query * Doctest added * Fix NameError for True/False in users.rst doctest Methods defined inside doctests have __globals__ pointing to the doctest globs dict. When called from within a Zope request handler, Python builtins are not resolved, causing NameError for True/False. Explicitly bind True and False into the doctest namespace before the adapter class definitions so they are found as globals at call time. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix NameError for True in to_dict by avoiding name lookup The doctest framework clears the globs dict after users.rst finishes, but DummyUserInfoAdapter stays registered in the global site manager. When auth.rst runs next and triggers a login, to_dict.__globals__ points to the cleared dict where True is no longer defined. Use (1 == 1) instead, which evaluates without a name lookup. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix CI: use current branch code instead of 2.x checkout The buildout.base.cfg has auto-checkout=* which caused mr.developer to checkout senaite.jsonapi from branch=2.x to src/senaite.jsonapi/, overriding the current branch checkout in '.'. Override auto-checkout to list all packages except senaite.jsonapi so that 'develop = .' uses the current branch's code for tests. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Revert CI fixes (moved to github-ci-fix branch) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Changelog updated --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 091def8 commit 4496cfa

4 files changed

Lines changed: 181 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+
- #83 Fetch multiple items by UID
78
- #80 Precise timestamp filtering and sorting for created/modified fields
89
- #79 Fix WrongType for UIDReferenceField
910
- #78 Enhance Users Resource with adapter-based info retrieval and filtering support

src/senaite/jsonapi/catalog.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,13 @@ def get_custom_query(self):
298298
date = api.calculate_since_date(modified_since)
299299
query["modified"] = {"query": date, "range": "min"}
300300

301+
# batch UID lookup: ?uids=uid1,uid2,uid3
302+
# UUIDIndex only supports 'query'/'range'/'not'; omitting 'operator'
303+
# uses the index default (or), which unions results across all UIDs
304+
uids = req.get_uids()
305+
if uids:
306+
query["UID"] = {"query": uids}
307+
301308
return query
302309

303310
def get_keyword_query(self, **kw):

src/senaite/jsonapi/request.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,18 @@ def get_modified_since():
216216
return get("modified_since", None)
217217

218218

219+
def get_uids():
220+
"""Returns a list of UIDs from the 'uids' request parameter.
221+
222+
Accepts a comma-separated string, e.g. ``?uids=uid1,uid2,uid3``.
223+
Returns an empty list when the parameter is absent or blank.
224+
"""
225+
value = get("uids", "")
226+
if not value:
227+
return []
228+
return [v.strip() for v in value.split(",") if v.strip()]
229+
230+
219231
def get_request_data():
220232
""" extract and convert the json data from the request
221233
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
BATCH FETCH BY UID
2+
------------------
3+
4+
Running this test from the buildout directory:
5+
6+
bin/test test_doctests -t batch_fetch
7+
8+
9+
Test Setup
10+
~~~~~~~~~~
11+
12+
Needed Imports:
13+
14+
>>> import json
15+
>>> import transaction
16+
>>> from plone.app.testing import setRoles
17+
>>> from plone.app.testing import TEST_USER_ID
18+
>>> from bika.lims import api
19+
20+
Functional Helpers:
21+
22+
>>> def get(url):
23+
... browser.open("{}/{}".format(api_url, url))
24+
... return browser.contents
25+
26+
>>> def get_count(response):
27+
... data = json.loads(response)
28+
... return data.get("count")
29+
30+
>>> def get_items_ids(response, sort=True):
31+
... data = json.loads(response)
32+
... items = data.get("items")
33+
... items = map(lambda it: it["id"], items)
34+
... if sort:
35+
... return sorted(items)
36+
... return list(items)
37+
38+
>>> def get_items_uids(response):
39+
... data = json.loads(response)
40+
... items = data.get("items")
41+
... return sorted(it["uid"] for it in items)
42+
43+
Variables:
44+
45+
>>> portal = self.portal
46+
>>> portal_url = portal.absolute_url()
47+
>>> api_url = "{}/@@API/senaite/v1".format(portal_url)
48+
>>> browser = self.getBrowser()
49+
>>> setRoles(portal, TEST_USER_ID, ["LabManager", "Manager"])
50+
51+
Create three Client objects and commit so they are indexed:
52+
53+
>>> c1 = api.create(portal.clients, "Client", title="Alpha Lab", ClientID="AL")
54+
>>> c2 = api.create(portal.clients, "Client", title="Beta Lab", ClientID="BL")
55+
>>> c3 = api.create(portal.clients, "Client", title="Gamma Lab", ClientID="GL")
56+
>>> uid1 = api.get_uid(c1)
57+
>>> uid2 = api.get_uid(c2)
58+
>>> uid3 = api.get_uid(c3)
59+
>>> transaction.commit()
60+
61+
62+
Batch fetch two objects by UID
63+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
64+
65+
Passing a comma-separated ``uids`` parameter returns only the matching
66+
objects in a single request:
67+
68+
>>> response = get("client?uids={},{}".format(uid1, uid2))
69+
>>> get_count(response)
70+
2
71+
>>> get_items_ids(response)
72+
[u'client-1', u'client-2']
73+
74+
The third client is not included because its UID was not requested:
75+
76+
>>> "Gamma Lab" in response
77+
False
78+
79+
80+
Batch fetch all three objects
81+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
82+
83+
>>> response = get("client?uids={},{},{}".format(uid1, uid2, uid3))
84+
>>> get_count(response)
85+
3
86+
>>> get_items_ids(response)
87+
[u'client-1', u'client-2', u'client-3']
88+
89+
90+
Batch fetch a single UID via the uids parameter
91+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
92+
93+
A single-item list is valid and returns one object wrapped in the
94+
standard batched response (with ``items`` and ``count``):
95+
96+
>>> response = get("client?uids={}".format(uid1))
97+
>>> get_count(response)
98+
1
99+
>>> get_items_ids(response)
100+
[u'client-1']
101+
102+
>>> "items" in response
103+
True
104+
105+
106+
Returned UIDs match the requested UIDs
107+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
108+
109+
>>> response = get("client?uids={},{}".format(uid2, uid3))
110+
>>> returned = get_items_uids(response)
111+
>>> returned == sorted([uid2, uid3])
112+
True
113+
114+
115+
Batch fetch works with complete=1
116+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
117+
118+
Adding ``complete=1`` wakes up the objects and returns full field data:
119+
120+
>>> response = get("client?uids={},{}&complete=1".format(uid1, uid2))
121+
>>> get_count(response)
122+
2
123+
>>> "Alpha Lab" in response
124+
True
125+
>>> "Beta Lab" in response
126+
True
127+
128+
129+
Batch fetch respects portal_type scoping
130+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
131+
132+
When using a typed resource route (``/client``), the portal_type
133+
constraint is applied alongside the UID filter. UIDs belonging to a
134+
different portal type are not returned.
135+
136+
Create a SampleType to obtain a non-Client UID:
137+
138+
>>> st = api.create(portal.setup.sampletypes, "SampleType", title="Blood", Prefix="BL")
139+
>>> uid_st = api.get_uid(st)
140+
>>> transaction.commit()
141+
142+
Fetching via ``/client?uids=`` with a SampleType UID returns no items:
143+
144+
>>> response = get("client?uids={}".format(uid_st))
145+
>>> get_count(response)
146+
0
147+
148+
149+
Batch fetch via the generic /search route
150+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
151+
152+
The ``uids`` parameter also works on the generic ``/search`` endpoint
153+
when combined with ``portal_type``:
154+
155+
>>> response = get(
156+
... "search?portal_type=Client&uids={},{}".format(uid1, uid3)
157+
... )
158+
>>> get_count(response)
159+
2
160+
>>> get_items_ids(response)
161+
[u'client-1', u'client-3']

0 commit comments

Comments
 (0)