@@ -145,17 +145,16 @@ But Sample Types are not stored in "senaite_catalog":
145145 []
146146
147147
148- Sorting by created and modified with second-level precision
149- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
148+ Date sorting and filtering with second-level precision
149+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
150150
151- When sorting by 'created' or 'modified' indexes, the sorting should have
152- second-level precision, not just minute-level precision. This ensures accurate
153- ordering of items created or modified within the same minute.
154-
155- Let's test this by creating multiple sample types in quick succession:
151+ DateIndex only stores minute-level precision, so objects created or modified
152+ seconds apart may share the same index value. The catalog post-processes
153+ results to achieve second-level accuracy for both sorting and filtering.
156154
157155 >>> import time
158156 >>> from DateTime import DateTime
157+ >>> from urllib import quote
159158
160159Create sample types with controlled timestamps to test second-level precision:
161160
@@ -205,10 +204,45 @@ Now test descending order:
205204 >>> [it[" title" ] for it in recent_items]
206205 [u'Gamma', u'Beta', u'Alpha']
207206
208- Now let's test sorting by 'modified'. The catalog implementation sorts DateIndex
209- results manually to achieve better precision than the default minute-level precision.
207+ The ``created_since `` parameter post-filters by the actual metadata value.
208+ Using Beta's creation time as cutoff returns Beta and Gamma (inclusive):
209+
210+ >>> cutoff = quote(created2.ISO8601())
211+ >>> response = get(" sampletype?created_since={} " .format(cutoff))
212+ >>> data = json.loads(response)
213+ >>> items = data.get(" items" ) or []
214+ >>> sorted ([it[" title" ] for it in items if it[" title" ] in titles])
215+ [u'Beta', u'Gamma']
210216
211- First, let's create new samples to modify with clear time separation:
217+ Using Gamma's creation time as cutoff returns only Gamma:
218+
219+ >>> cutoff = quote(created3.ISO8601())
220+ >>> response = get(" sampletype?created_since={} " .format(cutoff))
221+ >>> data = json.loads(response)
222+ >>> items = data.get(" items" ) or []
223+ >>> sorted ([it[" title" ] for it in items if it[" title" ] in titles])
224+ [u'Gamma']
225+
226+ Using Alpha's creation time as cutoff returns all three:
227+
228+ >>> cutoff = quote(created1.ISO8601())
229+ >>> response = get(" sampletype?created_since={} " .format(cutoff))
230+ >>> data = json.loads(response)
231+ >>> items = data.get(" items" ) or []
232+ >>> sorted ([it[" title" ] for it in items if it[" title" ] in titles])
233+ [u'Alpha', u'Beta', u'Gamma']
234+
235+ A future cutoff returns none of our items:
236+
237+ >>> future = quote(DateTime(created3 + 1.0 / 86400 ).ISO8601())
238+ >>> response = get(" sampletype?created_since={} " .format(future))
239+ >>> data = json.loads(response)
240+ >>> items = data.get(" items" ) or []
241+ >>> [it[" title" ] for it in items if it[" title" ] in titles]
242+ []
243+
244+ Sorting by ``modified `` also uses second-level precision. Create sample types
245+ and modify them with clear time separation:
212246
213247 >>> stm1 = api.create(portal.setup.sampletypes, " SampleType" , title = " ModAlpha" , Prefix = " MA" )
214248 >>> time.sleep(2 )
@@ -260,3 +294,158 @@ Verify timestamps are in descending order:
260294 >>> mod_times_desc = [DateTime(it[" modified" ]) for it in mod_items_desc]
261295 >>> mod_times_desc[0 ] >= mod_times_desc[1 ] >= mod_times_desc[2 ]
262296 True
297+
298+
299+ Custom DateIndex sorting (getDateReceived)
300+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
301+
302+ The second-level precision post-processing applies to any DateIndex field, not
303+ just ``created `` and ``modified ``. Here we test with ``getDateReceived ``, a
304+ custom DateIndex on the sample catalog.
305+
306+ Create the setup objects needed for sample creation:
307+
308+ >>> from bika.lims.utils.analysisrequest import create_analysisrequest
309+ >>> from bika.lims.workflow import doActionFor as do_action_for
310+
311+ >>> client = portal.clients[" client-1" ]
312+ >>> contact = api.create(client, " Contact" , Firstname = " Lab" , Surname = " User" )
313+ >>> sampletype = portal.setup.sampletypes[" sampletype-1" ]
314+ >>> category = api.create(portal.setup.analysiscategories,
315+ ... " AnalysisCategory" , title= " Chemistry" )
316+ >>> service = api.create(portal.bika_setup.bika_analysisservices,
317+ ... " AnalysisService" , title= " pH" , Keyword= " pH" ,
318+ ... Category= category, Price= " 10" )
319+ >>> transaction.commit()
320+
321+ Create three samples and receive them with 2-second gaps so that
322+ ``getDateReceived `` has distinct second-level timestamps:
323+
324+ >>> values = {
325+ ... " Contact" : api.get_uid(contact),
326+ ... " DateSampled" : DateTime().ISO8601(),
327+ ... " SampleType" : api.get_uid(sampletype),
328+ ... }
329+
330+ >>> request = self .request
331+ >>> s1 = create_analysisrequest(client, request, values, [api.get_uid(service)])
332+ >>> do_action_for(s1, " receive" )
333+ (...)
334+ >>> time.sleep(2 )
335+ >>> s2 = create_analysisrequest(client, request, values, [api.get_uid(service)])
336+ >>> do_action_for(s2, " receive" )
337+ (...)
338+ >>> time.sleep(2 )
339+ >>> s3 = create_analysisrequest(client, request, values, [api.get_uid(service)])
340+ >>> do_action_for(s3, " receive" )
341+ (...)
342+ >>> transaction.commit()
343+
344+ Verify the receive dates differ at second level:
345+
346+ >>> dr1 = s1.getDateReceived()
347+ >>> dr2 = s2.getDateReceived()
348+ >>> dr3 = s3.getDateReceived()
349+ >>> dr1 < dr2 < dr3
350+ True
351+
352+ >>> sample_ids = [s1.getId(), s2.getId(), s3.getId()]
353+
354+ Sorting by ``getDateReceived `` ascending returns samples in receive order:
355+
356+ >>> response = get(" search?portal_type=AnalysisRequest&sort_on=getDateReceived&sort_order=asc" )
357+ >>> data = json.loads(response)
358+ >>> items = data.get(" items" )
359+ >>> received = [it for it in items if it[" id" ] in sample_ids]
360+ >>> [it[" id" ] for it in received] == sample_ids
361+ True
362+
363+ Descending order:
364+
365+ >>> response = get(" search?portal_type=AnalysisRequest&sort_on=getDateReceived&sort_order=desc" )
366+ >>> data = json.loads(response)
367+ >>> items = data.get(" items" )
368+ >>> received = [it for it in items if it[" id" ] in sample_ids]
369+ >>> [it[" id" ] for it in received] == list (reversed (sample_ids))
370+ True
371+
372+
373+ Custom DateIndex filtering (getDateReceived)
374+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
375+
376+ The second-level precision post-filtering also works for date range queries
377+ on custom DateIndex fields. We reuse the samples created above (s1, s2, s3)
378+ with their distinct ``getDateReceived `` timestamps.
379+
380+ Using s2's receive date as the ``min `` cutoff returns s2 and s3 (inclusive
381+ lower bound):
382+
383+ >>> cutoff = quote(dr2.ISO8601())
384+ >>> url = " search?portal_type=AnalysisRequest&getDateReceived.query:record:list={} &getDateReceived.range:record=min" .format(cutoff)
385+ >>> response = get(url)
386+ >>> data = json.loads(response)
387+ >>> items = data.get(" items" ) or []
388+ >>> filtered = [it for it in items if it[" id" ] in sample_ids]
389+ >>> sorted ([it[" id" ] for it in filtered]) == sorted ([s2.getId(), s3.getId()])
390+ True
391+
392+ Using s3's receive date as the cutoff returns only s3:
393+
394+ >>> cutoff = quote(dr3.ISO8601())
395+ >>> url = " search?portal_type=AnalysisRequest&getDateReceived.query:record:list={} &getDateReceived.range:record=min" .format(cutoff)
396+ >>> response = get(url)
397+ >>> data = json.loads(response)
398+ >>> items = data.get(" items" ) or []
399+ >>> filtered = [it for it in items if it[" id" ] in sample_ids]
400+ >>> [it[" id" ] for it in filtered]
401+ [u'...']
402+ >>> filtered[0 ][" id" ] == s3.getId()
403+ True
404+
405+ Using s1's receive date returns all three:
406+
407+ >>> cutoff = quote(dr1.ISO8601())
408+ >>> url = " search?portal_type=AnalysisRequest&getDateReceived.query:record:list={} &getDateReceived.range:record=min" .format(cutoff)
409+ >>> response = get(url)
410+ >>> data = json.loads(response)
411+ >>> items = data.get(" items" ) or []
412+ >>> filtered = [it for it in items if it[" id" ] in sample_ids]
413+ >>> sorted ([it[" id" ] for it in filtered]) == sorted (sample_ids)
414+ True
415+
416+ Using ``max `` range with s2's receive date returns only s1 (exclusive upper
417+ bound):
418+
419+ >>> cutoff = quote(dr2.ISO8601())
420+ >>> url = " search?portal_type=AnalysisRequest&getDateReceived.query:record:list={} &getDateReceived.range:record=max" .format(cutoff)
421+ >>> response = get(url)
422+ >>> data = json.loads(response)
423+ >>> items = data.get(" items" ) or []
424+ >>> filtered = [it for it in items if it[" id" ] in sample_ids]
425+ >>> [it[" id" ] for it in filtered]
426+ [u'...']
427+ >>> filtered[0 ][" id" ] == s1.getId()
428+ True
429+
430+ Using ``min:max `` range with s1 and s3's receive dates returns s1 and s2
431+ (inclusive lower, exclusive upper):
432+
433+ >>> date_from = quote(dr1.ISO8601())
434+ >>> date_to = quote(dr3.ISO8601())
435+ >>> url = " search?portal_type=AnalysisRequest&getDateReceived.query:record:list={} &getDateReceived.query:record:list={} &getDateReceived.range:record=min:max" .format(date_from, date_to)
436+ >>> response = get(url)
437+ >>> data = json.loads(response)
438+ >>> items = data.get(" items" ) or []
439+ >>> filtered = [it for it in items if it[" id" ] in sample_ids]
440+ >>> sorted ([it[" id" ] for it in filtered]) == sorted ([s1.getId(), s2.getId()])
441+ True
442+
443+ A future cutoff returns none of our samples:
444+
445+ >>> future = quote(DateTime(dr3 + 1.0 / 86400 ).ISO8601())
446+ >>> url = " search?portal_type=AnalysisRequest&getDateReceived.query:record:list={} &getDateReceived.range:record=min" .format(future)
447+ >>> response = get(url)
448+ >>> data = json.loads(response)
449+ >>> items = data.get(" items" ) or []
450+ >>> [it[" id" ] for it in items if it[" id" ] in sample_ids]
451+ []
0 commit comments