| Home | Trees | Indices | Help |
|
|---|
|
|
1 """GNUmed measurements related business objects."""
2
3 # FIXME: use UCUM from Regenstrief Institute
4 #============================================================
5 __author__ = "K.Hilbert <Karsten.Hilbert@gmx.net>"
6 __license__ = "GPL"
7
8
9 import sys
10 import logging
11 import io
12 import decimal
13 import re as regex
14 import os.path
15
16
17 if __name__ == '__main__':
18 sys.path.insert(0, '../../')
19
20 from Gnumed.pycommon import gmDateTime
21 if __name__ == '__main__':
22 from Gnumed.pycommon import gmLog2
23 from Gnumed.pycommon import gmI18N
24 gmI18N.activate_locale()
25 gmI18N.install_domain('gnumed')
26 gmDateTime.init()
27 from Gnumed.pycommon import gmExceptions
28 from Gnumed.pycommon import gmBusinessDBObject
29 from Gnumed.pycommon import gmPG2
30 from Gnumed.pycommon import gmTools
31 from Gnumed.pycommon import gmDispatcher
32 from Gnumed.pycommon import gmHooks
33 from Gnumed.pycommon import gmCfg2
34
35 from Gnumed.business import gmOrganization
36 from Gnumed.business import gmCoding
37
38 _log = logging.getLogger('gm.lab')
39 _cfg = gmCfg2.gmCfgData()
40
41 #============================================================
42 HL7_RESULT_STATI = {
43 None: _('unknown'),
44 '': _('empty status'),
45 'C': _('C (HL7: Correction, replaces previous final)'),
46 'D': _('D (HL7: Deletion)'),
47 'F': _('F (HL7: Final)'),
48 'I': _('I (HL7: pending, specimen In lab)'),
49 'P': _('P (HL7: Preliminary)'),
50 'R': _('R (HL7: result entered, not yet verified)'),
51 'S': _('S (HL7: partial)'),
52 'X': _('X (HL7: cannot obtain results for this observation)'),
53 'U': _('U (HL7: mark as final (I/P/R/S -> F, value Unchanged)'),
54 'W': _('W (HL7: original Wrong (say, wrong patient))')
55 }
56
57 URL_test_result_information = 'http://www.laborlexikon.de'
58 URL_test_result_information_search = "http://www.google.de/search?as_oq=%(search_term)s&num=10&as_sitesearch=laborlexikon.de"
59
60 #============================================================
62 """Always relates to the active patient."""
63 gmHooks.run_hook_script(hook = 'after_test_result_modified')
64
65 gmDispatcher.connect(_on_test_result_modified, 'clin.test_result_mod_db')
66
67 #============================================================
68 _SQL_get_test_orgs = "SELECT * FROM clin.v_test_orgs WHERE %s"
69
71 """Represents one test org/lab."""
72 _cmd_fetch_payload = _SQL_get_test_orgs % 'pk_test_org = %s'
73 _cmds_store_payload = [
74 """UPDATE clin.test_org SET
75 fk_org_unit = %(pk_org_unit)s,
76 contact = gm.nullify_empty_string(%(test_org_contact)s),
77 comment = gm.nullify_empty_string(%(comment)s)
78 WHERE
79 pk = %(pk_test_org)s
80 AND
81 xmin = %(xmin_test_org)s
82 RETURNING
83 xmin AS xmin_test_org
84 """
85 ]
86 _updatable_fields = [
87 'pk_org_unit',
88 'test_org_contact',
89 'comment'
90 ]
91 #------------------------------------------------------------
93
94 _log.debug('creating test org [%s:%s:%s]', name, comment, pk_org_unit)
95
96 if name is None:
97 name = 'unassigned lab'
98
99 # get org unit
100 if pk_org_unit is None:
101 org = gmOrganization.create_org (
102 link_obj = link_obj,
103 organization = name,
104 category = 'Laboratory'
105 )
106 org_unit = gmOrganization.create_org_unit (
107 link_obj = link_obj,
108 pk_organization = org['pk_org'],
109 unit = name
110 )
111 pk_org_unit = org_unit['pk_org_unit']
112
113 # test org exists ?
114 args = {'pk_unit': pk_org_unit}
115 cmd = 'SELECT pk_test_org FROM clin.v_test_orgs WHERE pk_org_unit = %(pk_unit)s'
116 rows, idx = gmPG2.run_ro_queries(link_obj = link_obj, queries = [{'cmd': cmd, 'args': args}])
117
118 if len(rows) == 0:
119 cmd = 'INSERT INTO clin.test_org (fk_org_unit) VALUES (%(pk_unit)s) RETURNING pk'
120 rows, idx = gmPG2.run_rw_queries(link_obj = link_obj, queries = [{'cmd': cmd, 'args': args}], return_data = True)
121
122 test_org = cTestOrg(link_obj = link_obj, aPK_obj = rows[0][0])
123 if comment is not None:
124 comment = comment.strip()
125 test_org['comment'] = comment
126 test_org.save(conn = link_obj)
127
128 return test_org
129 #------------------------------------------------------------
131 args = {'pk': test_org}
132 cmd = """
133 DELETE FROM clin.test_org
134 WHERE
135 pk = %(pk)s
136 AND
137 NOT EXISTS (SELECT 1 FROM clin.lab_request WHERE fk_test_org = %(pk)s LIMIT 1)
138 AND
139 NOT EXISTS (SELECT 1 FROM clin.test_type WHERE fk_test_org = %(pk)s LIMIT 1)
140 """
141 gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
142 #------------------------------------------------------------
144 cmd = 'SELECT * FROM clin.v_test_orgs ORDER BY %s' % order_by
145 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd}], get_col_idx = True)
146 if return_pks:
147 return [ r['pk_test_org'] for r in rows ]
148 return [ cTestOrg(row = {'pk_field': 'pk_test_org', 'data': r, 'idx': idx}) for r in rows ]
149
150 #============================================================
151 # test panels / profiles
152 #------------------------------------------------------------
153 _SQL_get_test_panels = "SELECT * FROM clin.v_test_panels WHERE %s"
154
156 """Represents a grouping/listing of tests into a panel."""
157
158 _cmd_fetch_payload = _SQL_get_test_panels % "pk_test_panel = %s"
159 _cmds_store_payload = [
160 """
161 UPDATE clin.test_panel SET
162 description = gm.nullify_empty_string(%(description)s),
163 comment = gm.nullify_empty_string(%(comment)s)
164 WHERE
165 pk = %(pk_test_panel)s
166 AND
167 xmin = %(xmin_test_panel)s
168 RETURNING
169 xmin AS xmin_test_panel
170 """
171 ]
172 _updatable_fields = [
173 'description',
174 'comment'
175 ]
176 #--------------------------------------------------------
178 txt = _('Test panel "%s" [#%s]\n') % (
179 self._payload[self._idx['description']],
180 self._payload[self._idx['pk_test_panel']]
181 )
182
183 if self._payload[self._idx['comment']] is not None:
184 txt += '\n'
185 txt += gmTools.wrap (
186 text = self._payload[self._idx['comment']],
187 width = 50,
188 initial_indent = ' ',
189 subsequent_indent = ' '
190 )
191 txt += '\n'
192
193 txt += '\n'
194 txt += _('Includes:\n')
195 if len(self.included_loincs) == 0:
196 txt += _('no tests')
197 else:
198 tts_by_loinc = {}
199 for loinc in self._payload[self._idx['loincs']]:
200 tts_by_loinc[loinc] = []
201 for ttype in self.test_types:
202 tts_by_loinc[ttype['loinc']].append(ttype)
203 for loinc, ttypes in tts_by_loinc.items():
204 # maybe resolve LOINC, too
205 txt += _(' %s: %s\n') % (
206 loinc,
207 '; '.join([ '%(abbrev)s@%(name_org)s [#%(pk_test_type)s]' % tt for tt in ttypes ])
208 )
209
210 codes = self.generic_codes
211 if len(codes) > 0:
212 txt += '\n'
213 for c in codes:
214 txt += '%s %s: %s (%s - %s)\n' % (
215 (' ' * left_margin),
216 c['code'],
217 c['term'],
218 c['name_short'],
219 c['version']
220 )
221
222 return txt
223
224 #--------------------------------------------------------
226 """<pk_code> must be a value from ref.coding_system_root.pk_coding_system (clin.lnk_code2item_root.fk_generic_code)"""
227 cmd = "INSERT INTO clin.lnk_code2tst_pnl (fk_item, fk_generic_code) values (%(tp)s, %(code)s)"
228 args = {
229 'tp': self._payload[self._idx['pk_test_panel']],
230 'code': pk_code
231 }
232 rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
233 return True
234
235 #--------------------------------------------------------
237 """<pk_code> must be a value from ref.coding_system_root.pk_coding_system (clin.lnk_code2item_root.fk_generic_code)"""
238 cmd = "DELETE FROM clin.lnk_code2tst_pnl WHERE fk_item = %(tp)s AND fk_generic_code = %(code)s"
239 args = {
240 'tp': self._payload[self._idx['pk_test_panel']],
241 'code': pk_code
242 }
243 rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
244 return True
245
246 #--------------------------------------------------------
248 """Retrieve data about test types on this panel (for which this patient has results)."""
249
250 if order_by is None:
251 order_by = ''
252 else:
253 order_by = 'ORDER BY %s' % order_by
254
255 if unique_meta_types:
256 cmd = """
257 SELECT * FROM clin.v_test_types c_vtt
258 WHERE c_vtt.pk_test_type IN (
259 SELECT DISTINCT ON (c_vtr1.pk_meta_test_type) c_vtr1.pk_test_type
260 FROM clin.v_test_results c_vtr1
261 WHERE
262 c_vtr1.pk_test_type IN %%(pks)s
263 AND
264 c_vtr1.pk_patient = %%(pat)s
265 AND
266 c_vtr1.pk_meta_test_type IS NOT NULL
267 UNION ALL
268 SELECT DISTINCT ON (c_vtr2.pk_test_type) c_vtr2.pk_test_type
269 FROM clin.v_test_results c_vtr2
270 WHERE
271 c_vtr2.pk_test_type IN %%(pks)s
272 AND
273 c_vtr2.pk_patient = %%(pat)s
274 AND
275 c_vtr2.pk_meta_test_type IS NULL
276 )
277 %s""" % order_by
278 else:
279 cmd = """
280 SELECT * FROM clin.v_test_types c_vtt
281 WHERE c_vtt.pk_test_type IN (
282 SELECT DISTINCT ON (c_vtr.pk_test_type) c_vtr.pk_test_type
283 FROM clin.v_test_results c_vtr
284 WHERE
285 c_vtr.pk_test_type IN %%(pks)s
286 AND
287 c_vtr.pk_patient = %%(pat)s
288 )
289 %s""" % order_by
290
291 args = {
292 'pat': pk_patient,
293 'pks': tuple([ tt['pk_test_type'] for tt in self._payload[self._idx['test_types']] ])
294 }
295 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
296 return [ cMeasurementType(row = {'pk_field': 'pk_test_type', 'idx': idx, 'data': r}) for r in rows ]
297
298 #--------------------------------------------------------
300 if self._payload[self._idx['loincs']] is not None:
301 if loinc in self._payload[self._idx['loincs']]:
302 return
303 gmPG2.run_rw_queries(queries = [{
304 'cmd': 'INSERT INTO clin.lnk_loinc2test_panel (fk_test_panel, loinc) VALUES (%(pk_pnl)s, %(loinc)s)',
305 'args': {'loinc': loinc, 'pk_pnl': self._payload[self._idx['pk_test_panel']]}
306 }])
307 return
308
309 #--------------------------------------------------------
311 if self._payload[self._idx['loincs']] is None:
312 return
313 if loinc not in self._payload[self._idx['loincs']]:
314 return
315 gmPG2.run_rw_queries(queries = [{
316 'cmd': 'DELETE FROM clin.lnk_loinc2test_panel WHERE fk_test_panel = %(pk_pnl)s AND loinc = %(loinc)s',
317 'args': {'loinc': loinc, 'pk_pnl': self._payload[self._idx['pk_test_panel']]}
318 }])
319 return
320
321 #--------------------------------------------------------
322 # properties
323 #--------------------------------------------------------
326
328 queries = []
329 # remove those which don't belong
330 if len(loincs) == 0:
331 cmd = 'DELETE FROM clin.lnk_loinc2test_panel WHERE fk_test_panel = %(pk_pnl)s'
332 else:
333 cmd = 'DELETE FROM clin.lnk_loinc2test_panel WHERE fk_test_panel = %(pk_pnl)s AND loinc NOT IN %(loincs)s'
334 queries.append({'cmd': cmd, 'args': {'loincs': tuple(loincs), 'pk_pnl': self._payload[self._idx['pk_test_panel']]}})
335 # add those not there yet
336 if len(loincs) > 0:
337 for loinc in loincs:
338 cmd = """INSERT INTO clin.lnk_loinc2test_panel (fk_test_panel, loinc)
339 SELECT %(pk_pnl)s, %(loinc)s WHERE NOT EXISTS (
340 SELECT 1 FROM clin.lnk_loinc2test_panel WHERE
341 fk_test_panel = %(pk_pnl)s
342 AND
343 loinc = %(loinc)s
344 )"""
345 queries.append({'cmd': cmd, 'args': {'loinc': loinc, 'pk_pnl': self._payload[self._idx['pk_test_panel']]}})
346 return gmPG2.run_rw_queries(queries = queries)
347
348 included_loincs = property(_get_included_loincs, _set_included_loincs)
349
350 #--------------------------------------------------------
352 if len(self._payload[self._idx['test_types']]) == 0:
353 return []
354
355 rows, idx = gmPG2.run_ro_queries (
356 queries = [{
357 'cmd': _SQL_get_test_types % 'pk_test_type IN %(pks)s ORDER BY unified_abbrev',
358 'args': {'pks': tuple([ tt['pk_test_type'] for tt in self._payload[self._idx['test_types']] ])}
359 }],
360 get_col_idx = True
361 )
362 return [ cMeasurementType(row = {'data': r, 'idx': idx, 'pk_field': 'pk_test_type'}) for r in rows ]
363
364 test_types = property(_get_test_types, lambda x:x)
365
366 #--------------------------------------------------------
368 if len(self._payload[self._idx['pk_generic_codes']]) == 0:
369 return []
370
371 cmd = gmCoding._SQL_get_generic_linked_codes % 'pk_generic_code IN %(pks)s'
372 args = {'pks': tuple(self._payload[self._idx['pk_generic_codes']])}
373 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
374 return [ gmCoding.cGenericLinkedCode(row = {'data': r, 'idx': idx, 'pk_field': 'pk_lnk_code2item'}) for r in rows ]
375
377 queries = []
378 # remove all codes
379 if len(self._payload[self._idx['pk_generic_codes']]) > 0:
380 queries.append ({
381 'cmd': 'DELETE FROM clin.lnk_code2tst_pnl WHERE fk_item = %(tp)s AND fk_generic_code IN %(codes)s',
382 'args': {
383 'tp': self._payload[self._idx['pk_test_panel']],
384 'codes': tuple(self._payload[self._idx['pk_generic_codes']])
385 }
386 })
387 # add new codes
388 for pk_code in pk_codes:
389 queries.append ({
390 'cmd': 'INSERT INTO clin.lnk_code2test_panel (fk_item, fk_generic_code) VALUES (%(tp)s, %(pk_code)s)',
391 'args': {
392 'tp': self._payload[self._idx['pk_test_panel']],
393 'pk_code': pk_code
394 }
395 })
396 if len(queries) == 0:
397 return
398 # run it all in one transaction
399 rows, idx = gmPG2.run_rw_queries(queries = queries)
400 return
401
402 generic_codes = property(_get_generic_codes, _set_generic_codes)
403
404 #--------------------------------------------------------
405 - def get_most_recent_results(self, pk_patient=None, order_by=None, group_by_meta_type=False, include_missing=False):
406
407 if len(self._payload[self._idx['test_types']]) == 0:
408 return []
409
410 pnl_results = get_most_recent_results_for_panel (
411 pk_patient = pk_patient,
412 pk_panel = self._payload[self._idx['pk_test_panel']],
413 order_by = order_by,
414 group_by_meta_type = group_by_meta_type
415 )
416 if not include_missing:
417 return pnl_results
418
419 loincs_found = [ r['loinc_tt'] for r in pnl_results ]
420 loincs_found.extend([ r['loinc_meta'] for r in pnl_results if r['loinc_meta'] not in loincs_found ])
421 loincs2consider = set([ tt['loinc'] for tt in self._payload[self._idx['test_types']] ])
422 loincs_missing = loincs2consider - set(loincs_found)
423 pnl_results.extend(loincs_missing)
424 return pnl_results
425
426 #------------------------------------------------------------
428 where_args = {}
429 if loincs is None:
430 where_parts = ['true']
431 else:
432 where_parts = ['loincs @> %(loincs)s']
433 where_args['loincs'] = list(loincs)
434
435 if order_by is None:
436 order_by = u''
437 else:
438 order_by = ' ORDER BY %s' % order_by
439
440 cmd = (_SQL_get_test_panels % ' AND '.join(where_parts)) + order_by
441 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': where_args}], get_col_idx = True)
442 if return_pks:
443 return [ r['pk_test_panel'] for r in rows ]
444 return [ cTestPanel(row = {'data': r, 'idx': idx, 'pk_field': 'pk_test_panel'}) for r in rows ]
445
446 #------------------------------------------------------------
448
449 args = {'desc': description.strip()}
450 cmd = """
451 INSERT INTO clin.test_panel (description)
452 VALUES (gm.nullify_empty_string(%(desc)s))
453 RETURNING pk
454 """
455 rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}], return_data = True, get_col_idx = False)
456
457 return cTestPanel(aPK_obj = rows[0]['pk'])
458
459 #------------------------------------------------------------
461 args = {'pk': pk}
462 cmd = "DELETE FROM clin.test_panel WHERE pk = %(pk)s"
463 gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
464 return True
465
466 #============================================================
468 """Represents one meta test type under which actual test types can be aggregated."""
469
470 _cmd_fetch_payload = "SELECT *, xmin FROM clin.meta_test_type WHERE pk = %s"
471 _cmds_store_payload = ["""
472 UPDATE clin.meta_test_type SET
473 abbrev = %(abbrev)s,
474 name = %(name)s,
475 loinc = gm.nullify_empty_string(%(loinc)s),
476 comment = gm.nullify_empty_string(%(comment)s)
477 WHERE
478 pk = %(pk)s
479 AND
480 xmin = %(xmin)s
481 RETURNING
482 xmin
483 """]
484 _updatable_fields = [
485 'abbrev',
486 'name',
487 'loinc',
488 'comment'
489 ]
490 #--------------------------------------------------------
492 txt = _('Meta (%s=aggregate) test type [#%s]\n\n') % (gmTools.u_sum, self._payload[self._idx['pk']])
493 txt += _(' Name: %s (%s)\n') % (
494 self._payload[self._idx['abbrev']],
495 self._payload[self._idx['name']]
496 )
497 if self._payload[self._idx['loinc']] is not None:
498 txt += ' LOINC: %s\n' % self._payload[self._idx['loinc']]
499 if self._payload[self._idx['comment']] is not None:
500 txt += _(' Comment: %s\n') % self._payload[self._idx['comment']]
501 if with_tests:
502 ttypes = self.included_test_types
503 if len(ttypes) > 0:
504 txt += _(' Aggregates the following test types:\n')
505 for ttype in ttypes:
506 txt += ' - %s (%s)%s%s%s [#%s]\n' % (
507 ttype['name'],
508 ttype['abbrev'],
509 gmTools.coalesce(ttype['reference_unit'], '', ', %s'),
510 gmTools.coalesce(ttype['name_org'], '', ' (%s)'),
511 gmTools.coalesce(ttype['loinc'], '', ', LOINC: %s'),
512 ttype['pk_test_type']
513 )
514 if patient is not None:
515 txt += '\n'
516 most_recent = self.get_most_recent_result(patient = patient)
517 if most_recent is not None:
518 txt += _(' Most recent (%s): %s%s%s') % (
519 gmDateTime.pydt_strftime(most_recent['clin_when'], '%Y %b %d'),
520 most_recent['unified_val'],
521 gmTools.coalesce(most_recent['val_unit'], '', ' %s'),
522 gmTools.coalesce(most_recent['abnormality_indicator'], '', ' (%s)')
523 )
524 oldest = self.get_oldest_result(patient = patient)
525 if oldest is not None:
526 txt += '\n'
527 txt += _(' Oldest (%s): %s%s%s') % (
528 gmDateTime.pydt_strftime(oldest['clin_when'], '%Y %b %d'),
529 oldest['unified_val'],
530 gmTools.coalesce(oldest['val_unit'], '', ' %s'),
531 gmTools.coalesce(oldest['abnormality_indicator'], '', ' (%s)')
532 )
533 return txt
534
535 #--------------------------------------------------------
537 args = {
538 'pat': patient,
539 'mttyp': self._payload[self._idx['pk']]
540 }
541 cmd = """
542 SELECT * FROM clin.v_test_results
543 WHERE
544 pk_patient = %(pat)s
545 AND
546 pk_meta_test_type = %(mttyp)s
547 ORDER BY clin_when DESC
548 LIMIT 1"""
549 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
550 if len(rows) == 0:
551 return None
552
553 return cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': rows[0]})
554
555 #--------------------------------------------------------
557 args = {
558 'pat': patient,
559 'mttyp': self._payload[self._idx['pk']]
560 }
561 cmd = """
562 SELECT * FROM clin.v_test_results
563 WHERE
564 pk_patient = %(pat)s
565 AND
566 pk_meta_test_type = %(mttyp)s
567 ORDER BY clin_when
568 LIMIT 1"""
569 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
570 if len(rows) == 0:
571 return None
572
573 return cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': rows[0]})
574
575 #--------------------------------------------------------
577
578 args = {
579 'pat': pk_patient,
580 'mtyp': self._payload[self._idx['pk']],
581 'mloinc': self._payload[self._idx['loinc']],
582 'when': date
583 }
584 WHERE_meta = ''
585
586 SQL = """
587 SELECT * FROM clin.v_test_results
588 WHERE
589 pk_patient = %%(pat)s
590 AND
591 clin_when %s %%(when)s
592 AND
593 ((pk_meta_test_type = %%(mtyp)s) OR (loinc_meta = %%(mloinc)s))
594 ORDER BY clin_when
595 LIMIT 1"""
596
597 # get earlier results by meta type
598 earlier_result = None
599 cmd = SQL % '<'
600 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
601 if len(rows) > 0:
602 earlier_result = cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': rows[0]})
603
604 # get later results by meta type ?
605 later_result = None
606 cmd = SQL % '>'
607 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
608 if len(rows) > 0:
609 later_result = cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': rows[0]})
610
611 if earlier_result is None:
612 return later_result
613 if later_result is None:
614 return earlier_result
615
616 earlier_ago = date - earlier_result['clin_when']
617 later_ago = later_result['clin_when'] - date
618 if earlier_ago < later_ago:
619 return earlier_result
620 return later_result
621
622 #--------------------------------------------------------
623 # properties
624 #--------------------------------------------------------
626 cmd = _SQL_get_test_types % 'pk_meta_test_type = %(pk_meta)s'
627 args = {'pk_meta': self._payload[self._idx['pk']]}
628 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
629 return [ cMeasurementType(row = {'pk_field': 'pk_test_type', 'data': r, 'idx': idx}) for r in rows ]
630
631 included_test_types = property(_get_included_test_types, lambda x:x)
632
633 #------------------------------------------------------------
635 cmd = """
636 INSERT INTO clin.meta_test_type (name, abbrev)
637 SELECT
638 %(name)s,
639 %(abbr)s
640 WHERE NOT EXISTS (
641 SELECT 1 FROM clin.meta_test_type
642 WHERE
643 name = %(name)s
644 AND
645 abbrev = %(abbr)s
646 )
647 RETURNING *, xmin
648 """
649 args = {
650 'name': name.strip(),
651 'abbr': abbreviation.strip()
652 }
653 rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True, return_data = True)
654 if len(rows) == 0:
655 if not return_existing:
656 return None
657 cmd = "SELECT *, xmin FROM clin.meta_test_type WHERE name = %(name)s and %(abbr)s"
658 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
659
660 return cMetaTestType(row = {'pk_field': 'pk', 'idx': idx, 'data': rows[0]})
661
662 #------------------------------------------------------------
664 cmd = """
665 DELETE FROM clin.meta_test_type
666 WHERE
667 pk = %(pk)s
668 AND
669 NOT EXISTS (
670 SELECT 1 FROM clin.test_type
671 WHERE fk_meta_test_type = %(pk)s
672 )"""
673 args = {'pk': meta_type}
674 gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
675
676 #------------------------------------------------------------
678 cmd = 'SELECT *, xmin FROM clin.meta_test_type'
679 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd}], get_col_idx = True)
680 if return_pks:
681 return [ r['pk'] for r in rows ]
682 return [ cMetaTestType(row = {'pk_field': 'pk', 'data': r, 'idx': idx}) for r in rows ]
683
684 #============================================================
685 _SQL_get_test_types = "SELECT * FROM clin.v_test_types WHERE %s"
686
688 """Represents one test result type."""
689
690 _cmd_fetch_payload = _SQL_get_test_types % "pk_test_type = %s"
691
692 _cmds_store_payload = [
693 """UPDATE clin.test_type SET
694 abbrev = gm.nullify_empty_string(%(abbrev)s),
695 name = gm.nullify_empty_string(%(name)s),
696 loinc = gm.nullify_empty_string(%(loinc)s),
697 comment = gm.nullify_empty_string(%(comment_type)s),
698 reference_unit = gm.nullify_empty_string(%(reference_unit)s),
699 fk_test_org = %(pk_test_org)s,
700 fk_meta_test_type = %(pk_meta_test_type)s
701 WHERE
702 pk = %(pk_test_type)s
703 AND
704 xmin = %(xmin_test_type)s
705 RETURNING
706 xmin AS xmin_test_type"""
707 ]
708
709 _updatable_fields = [
710 'abbrev',
711 'name',
712 'loinc',
713 'comment_type',
714 'reference_unit',
715 'pk_test_org',
716 'pk_meta_test_type'
717 ]
718 #--------------------------------------------------------
719 # properties
720 #--------------------------------------------------------
722 cmd = 'SELECT EXISTS(SELECT 1 FROM clin.test_result WHERE fk_type = %(pk_type)s)'
723 args = {'pk_type': self._payload[self._idx['pk_test_type']]}
724 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}])
725 return rows[0][0]
726
727 in_use = property(_get_in_use, lambda x:x)
728
729 #--------------------------------------------------------
731 results = get_most_recent_results_for_test_type (
732 test_type = self._payload[self._idx['pk_test_type']],
733 max_no_of_results = max_no_of_results,
734 patient = patient
735 )
736 if len(results) > 0:
737 return results
738
739 if self._payload[self._idx['loinc']] is None:
740 return []
741
742 return get_most_recent_results_in_loinc_group (
743 loincs = tuple(self._payload[self._idx['loinc']]),
744 max_no_of_results = max_no_of_results,
745 patient = patient
746 # ?
747 )
748
749 #--------------------------------------------------------
751 result = get_oldest_result (
752 test_type = self._payload[self._idx['pk_test_type']],
753 loinc = None,
754 patient = patient
755 )
756 if result is None:
757 if self._payload[self._idx['loinc']] is not None:
758 result = get_oldest_result (
759 test_type = None,
760 loinc = self._payload[self._idx['loinc']],
761 patient = patient
762 )
763 return result
764
765 #--------------------------------------------------------
767 if self._payload[self._idx['pk_test_panels']] is None:
768 return None
769
770 return [ cTestPanel(aPK_obj = pk) for pk in self._payload[self._idx['pk_test_panels']] ]
771
772 test_panels = property(_get_test_panels, lambda x:x)
773
774 #--------------------------------------------------------
776 if real_one_only is False:
777 return cMetaTestType(aPK_obj = self._payload[self._idx['pk_meta_test_type']])
778 if self._payload[self._idx['is_fake_meta_type']]:
779 return None
780 return cMetaTestType(aPK_obj = self._payload[self._idx['pk_meta_test_type']])
781
782 meta_test_type = property(get_meta_test_type, lambda x:x)
783
784 #--------------------------------------------------------
786 """Returns the closest test result which does have normal range information.
787
788 - needs <unit>
789 - if <timestamp> is None it will assume now() and thus return the most recent
790 """
791 if timestamp is None:
792 timestamp = gmDateTime.pydt_now_here()
793 cmd = """
794 SELECT * FROM clin.v_test_results
795 WHERE
796 pk_test_type = %(pk_type)s
797 AND
798 val_unit = %(unit)s
799 AND
800 (
801 (val_normal_min IS NOT NULL)
802 OR
803 (val_normal_max IS NOT NULL)
804 OR
805 (val_normal_range IS NOT NULL)
806 )
807 ORDER BY
808 CASE
809 WHEN clin_when > %(clin_when)s THEN clin_when - %(clin_when)s
810 ELSE %(clin_when)s - clin_when
811 END
812 LIMIT 1"""
813 args = {
814 'pk_type': self._payload[self._idx['pk_test_type']],
815 'unit': unit,
816 'clin_when': timestamp
817 }
818 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
819 if len(rows) == 0:
820 return None
821 r = rows[0]
822 return cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': r})
823
824 #--------------------------------------------------------
826 """Returns the closest test result which does have target range information.
827
828 - needs <unit>
829 - needs <patient> (as target will be per-patient)
830 - if <timestamp> is None it will assume now() and thus return the most recent
831 """
832 if timestamp is None:
833 timestamp = gmDateTime.pydt_now_here()
834 cmd = """
835 SELECT * FROM clin.v_test_results
836 WHERE
837 pk_test_type = %(pk_type)s
838 AND
839 val_unit = %(unit)s
840 AND
841 pk_patient = %(pat)s
842 AND
843 (
844 (val_target_min IS NOT NULL)
845 OR
846 (val_target_max IS NOT NULL)
847 OR
848 (val_target_range IS NOT NULL)
849 )
850 ORDER BY
851 CASE
852 WHEN clin_when > %(clin_when)s THEN clin_when - %(clin_when)s
853 ELSE %(clin_when)s - clin_when
854 END
855 LIMIT 1"""
856 args = {
857 'pk_type': self._payload[self._idx['pk_test_type']],
858 'unit': unit,
859 'pat': patient,
860 'clin_when': timestamp
861 }
862 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
863 if len(rows) == 0:
864 return None
865 r = rows[0]
866 return cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': r})
867
868 #--------------------------------------------------------
870 """Returns the unit of the closest test result.
871
872 - if <timestamp> is None it will assume now() and thus return the most recent
873 """
874 if timestamp is None:
875 timestamp = gmDateTime.pydt_now_here()
876 cmd = """
877 SELECT val_unit FROM clin.v_test_results
878 WHERE
879 pk_test_type = %(pk_type)s
880 AND
881 val_unit IS NOT NULL
882 ORDER BY
883 CASE
884 WHEN clin_when > %(clin_when)s THEN clin_when - %(clin_when)s
885 ELSE %(clin_when)s - clin_when
886 END
887 LIMIT 1"""
888 args = {
889 'pk_type': self._payload[self._idx['pk_test_type']],
890 'clin_when': timestamp
891 }
892 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
893 if len(rows) == 0:
894 return None
895 return rows[0]['val_unit']
896
897 temporally_closest_unit = property(get_temporally_closest_unit, lambda x:x)
898
899 #--------------------------------------------------------
901 tt = ''
902 tt += _('Test type "%s" (%s) [#%s]\n') % (
903 self._payload[self._idx['name']],
904 self._payload[self._idx['abbrev']],
905 self._payload[self._idx['pk_test_type']]
906 )
907 tt += '\n'
908 tt += gmTools.coalesce(self._payload[self._idx['loinc']], '', ' LOINC: %s\n')
909 tt += gmTools.coalesce(self._payload[self._idx['reference_unit']], '', _(' Reference unit: %s\n'))
910 tt += gmTools.coalesce(self._payload[self._idx['comment_type']], '', _(' Comment: %s\n'))
911
912 tt += '\n'
913 tt += _('Lab details:\n')
914 tt += _(' Name: %s\n') % gmTools.coalesce(self._payload[self._idx['name_org']], '')
915 tt += gmTools.coalesce(self._payload[self._idx['contact_org']], '', _(' Contact: %s\n'))
916 tt += gmTools.coalesce(self._payload[self._idx['comment_org']], '', _(' Comment: %s\n'))
917
918 if self._payload[self._idx['is_fake_meta_type']] is False:
919 tt += '\n'
920 tt += _('Aggregated under meta type:\n')
921 tt += _(' Name: %s - %s [#%s]\n') % (
922 self._payload[self._idx['abbrev_meta']],
923 self._payload[self._idx['name_meta']],
924 self._payload[self._idx['pk_meta_test_type']]
925 )
926 tt += gmTools.coalesce(self._payload[self._idx['loinc_meta']], '', ' LOINC: %s\n')
927 tt += gmTools.coalesce(self._payload[self._idx['comment_meta']], '', _(' Comment: %s\n'))
928
929 panels = self.test_panels
930 if panels is not None:
931 tt += '\n'
932 tt += _('Listed in test panels:\n')
933 for panel in panels:
934 tt += _(' Panel "%s" [#%s]\n') % (
935 panel['description'],
936 panel['pk_test_panel']
937 )
938
939 if patient is not None:
940 tt += '\n'
941 results = self.get_most_recent_results(patient = patient, max_no_of_results = 1)
942 if len(results) > 0:
943 result = results[0]
944 tt += _(' Most recent (%s): %s%s%s') % (
945 gmDateTime.pydt_strftime(result['clin_when'], '%Y %b %d'),
946 result['unified_val'],
947 gmTools.coalesce(result['val_unit'], '', ' %s'),
948 gmTools.coalesce(result['abnormality_indicator'], '', ' (%s)')
949 )
950 result = self.get_oldest_result(patient = patient)
951 if result is not None:
952 tt += '\n'
953 tt += _(' Oldest (%s): %s%s%s') % (
954 gmDateTime.pydt_strftime(result['clin_when'], '%Y %b %d'),
955 result['unified_val'],
956 gmTools.coalesce(result['val_unit'], '', ' %s'),
957 gmTools.coalesce(result['abnormality_indicator'], '', ' (%s)')
958 )
959
960 return tt
961
962 #------------------------------------------------------------
964 args = {}
965 where_parts = []
966 if loincs is not None:
967 if len(loincs) > 0:
968 where_parts.append('loinc IN %(loincs)s')
969 args['loincs'] = tuple(loincs)
970 if len(where_parts) == 0:
971 where_parts.append('TRUE')
972 WHERE_clause = ' AND '.join(where_parts)
973 cmd = (_SQL_get_test_types % WHERE_clause) + gmTools.coalesce(order_by, '', ' ORDER BY %s')
974 #cmd = 'select * from clin.v_test_types %s' % gmTools.coalesce(order_by, '', 'order by %s')
975 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
976 if return_pks:
977 return [ r['pk_test_type'] for r in rows ]
978 return [ cMeasurementType(row = {'pk_field': 'pk_test_type', 'data': r, 'idx': idx}) for r in rows ]
979
980 #------------------------------------------------------------
982
983 if (abbrev is None) and (name is None):
984 raise ValueError('must have <abbrev> and/or <name> set')
985
986 where_snippets = []
987
988 if lab is None:
989 where_snippets.append('pk_test_org IS NULL')
990 else:
991 try:
992 int(lab)
993 where_snippets.append('pk_test_org = %(lab)s')
994 except (TypeError, ValueError):
995 where_snippets.append('pk_test_org = (SELECT pk_test_org FROM clin.v_test_orgs WHERE unit = %(lab)s)')
996
997 if abbrev is not None:
998 where_snippets.append('abbrev = %(abbrev)s')
999
1000 if name is not None:
1001 where_snippets.append('name = %(name)s')
1002
1003 where_clause = ' and '.join(where_snippets)
1004 cmd = "select * from clin.v_test_types where %s" % where_clause
1005 args = {'lab': lab, 'abbrev': abbrev, 'name': name}
1006
1007 rows, idx = gmPG2.run_ro_queries(link_obj = link_obj, queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
1008
1009 if len(rows) == 0:
1010 return None
1011
1012 tt = cMeasurementType(row = {'pk_field': 'pk_test_type', 'data': rows[0], 'idx': idx})
1013 return tt
1014
1015 #------------------------------------------------------------
1017 cmd = 'delete from clin.test_type where pk = %(pk)s'
1018 args = {'pk': measurement_type}
1019 gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
1020
1021 #------------------------------------------------------------
1023 """Create or get test type."""
1024
1025 ttype = find_measurement_type(lab = lab, abbrev = abbrev, name = name, link_obj = link_obj)
1026 # found ?
1027 if ttype is not None:
1028 return ttype
1029
1030 _log.debug('creating test type [%s:%s:%s:%s]', lab, abbrev, name, unit)
1031
1032 # not found, so create it
1033 # if unit is None:
1034 # _log.error('need <unit> to create test type: %s:%s:%s:%s' % (lab, abbrev, name, unit))
1035 # raise ValueError('need <unit> to create test type')
1036
1037 # make query
1038 cols = []
1039 val_snippets = []
1040 vals = {}
1041
1042 # lab
1043 if lab is None:
1044 lab = create_test_org(link_obj = link_obj)['pk_test_org']
1045
1046 cols.append('fk_test_org')
1047 try:
1048 vals['lab'] = int(lab)
1049 val_snippets.append('%(lab)s')
1050 except Exception:
1051 vals['lab'] = lab
1052 val_snippets.append('(SELECT pk_test_org FROM clin.v_test_orgs WHERE unit = %(lab)s)')
1053
1054 # code
1055 cols.append('abbrev')
1056 val_snippets.append('%(abbrev)s')
1057 vals['abbrev'] = abbrev
1058
1059 # unit
1060 if unit is not None:
1061 cols.append('reference_unit')
1062 val_snippets.append('%(unit)s')
1063 vals['unit'] = unit
1064
1065 # name
1066 if name is not None:
1067 cols.append('name')
1068 val_snippets.append('%(name)s')
1069 vals['name'] = name
1070
1071 col_clause = ', '.join(cols)
1072 val_clause = ', '.join(val_snippets)
1073 queries = [
1074 {'cmd': 'insert into clin.test_type (%s) values (%s)' % (col_clause, val_clause), 'args': vals},
1075 {'cmd': "select * from clin.v_test_types where pk_test_type = currval(pg_get_serial_sequence('clin.test_type', 'pk'))"}
1076 ]
1077 rows, idx = gmPG2.run_rw_queries(link_obj = link_obj, queries = queries, get_col_idx = True, return_data = True)
1078 ttype = cMeasurementType(row = {'pk_field': 'pk_test_type', 'data': rows[0], 'idx': idx})
1079
1080 return ttype
1081
1082 #============================================================
1084 """Represents one test result."""
1085
1086 _cmd_fetch_payload = "select * from clin.v_test_results where pk_test_result = %s"
1087
1088 _cmds_store_payload = [
1089 """UPDATE clin.test_result SET
1090 clin_when = %(clin_when)s,
1091 narrative = nullif(trim(%(comment)s), ''),
1092 val_num = %(val_num)s,
1093 val_alpha = nullif(trim(%(val_alpha)s), ''),
1094 val_unit = nullif(trim(%(val_unit)s), ''),
1095 val_normal_min = %(val_normal_min)s,
1096 val_normal_max = %(val_normal_max)s,
1097 val_normal_range = nullif(trim(%(val_normal_range)s), ''),
1098 val_target_min = %(val_target_min)s,
1099 val_target_max = %(val_target_max)s,
1100 val_target_range = nullif(trim(%(val_target_range)s), ''),
1101 abnormality_indicator = nullif(trim(%(abnormality_indicator)s), ''),
1102 norm_ref_group = nullif(trim(%(norm_ref_group)s), ''),
1103 note_test_org = nullif(trim(%(note_test_org)s), ''),
1104 material = nullif(trim(%(material)s), ''),
1105 material_detail = nullif(trim(%(material_detail)s), ''),
1106 status = gm.nullify_empty_string(%(status)s),
1107 val_grouping = gm.nullify_empty_string(%(val_grouping)s),
1108 source_data = gm.nullify_empty_string(%(source_data)s),
1109 fk_intended_reviewer = %(pk_intended_reviewer)s,
1110 fk_encounter = %(pk_encounter)s,
1111 fk_episode = %(pk_episode)s,
1112 fk_type = %(pk_test_type)s,
1113 fk_request = %(pk_request)s
1114 WHERE
1115 pk = %(pk_test_result)s AND
1116 xmin = %(xmin_test_result)s
1117 RETURNING
1118 xmin AS xmin_test_result
1119 """
1120 # , u"""select xmin_test_result from clin.v_test_results where pk_test_result = %(pk_test_result)s"""
1121 ]
1122
1123 _updatable_fields = [
1124 'clin_when',
1125 'comment',
1126 'val_num',
1127 'val_alpha',
1128 'val_unit',
1129 'val_normal_min',
1130 'val_normal_max',
1131 'val_normal_range',
1132 'val_target_min',
1133 'val_target_max',
1134 'val_target_range',
1135 'abnormality_indicator',
1136 'norm_ref_group',
1137 'note_test_org',
1138 'material',
1139 'material_detail',
1140 'status',
1141 'val_grouping',
1142 'source_data',
1143 'pk_intended_reviewer',
1144 'pk_encounter',
1145 'pk_episode',
1146 'pk_test_type',
1147 'pk_request'
1148 ]
1149
1150 #--------------------------------------------------------
1152 range_info = gmTools.coalesce (
1153 self.formatted_clinical_range,
1154 self.formatted_normal_range
1155 )
1156 review = gmTools.bool2subst (
1157 self._payload[self._idx['reviewed']],
1158 '',
1159 ' ' + gmTools.u_writing_hand,
1160 ' ' + gmTools.u_writing_hand
1161 )
1162 txt = '%s %s: %s%s%s%s%s%s' % (
1163 gmDateTime.pydt_strftime (
1164 self._payload[self._idx['clin_when']],
1165 date_format
1166 ),
1167 self._payload[self._idx['name_tt']],
1168 self._payload[self._idx['unified_val']],
1169 gmTools.coalesce(self._payload[self._idx['val_unit']], '', ' %s'),
1170 gmTools.coalesce(self._payload[self._idx['abnormality_indicator']], '', ' %s'),
1171 gmTools.coalesce(range_info, '', ' (%s)'),
1172 gmTools.coalesce(self._payload[self._idx['status']], '', ' [%s]')[:2],
1173 review
1174 )
1175 if with_notes:
1176 txt += '\n'
1177 if self._payload[self._idx['note_test_org']] is not None:
1178 txt += ' ' + _('Lab comment: %s\n') % _('\n Lab comment: ').join(self._payload[self._idx['note_test_org']].split('\n'))
1179 if self._payload[self._idx['comment']] is not None:
1180 txt += ' ' + _('Praxis comment: %s\n') % _('\n Praxis comment: ').join(self._payload[self._idx['comment']].split('\n'))
1181
1182 return txt.strip('\n')
1183
1184 #--------------------------------------------------------
1185 - def format(self, with_review=True, with_evaluation=True, with_ranges=True, with_episode=True, with_type_details=True, with_source_data=False, date_format='%Y %b %d %H:%M'):
1186
1187 # FIXME: add battery, request details
1188
1189 # header
1190 tt = _('Result from %s \n') % gmDateTime.pydt_strftime (
1191 self._payload[self._idx['clin_when']],
1192 date_format
1193 )
1194
1195 # basics
1196 tt += ' ' + _('Type: "%(name)s" (%(abbr)s) [#%(pk_type)s]\n') % ({
1197 'name': self._payload[self._idx['name_tt']],
1198 'abbr': self._payload[self._idx['abbrev_tt']],
1199 'pk_type': self._payload[self._idx['pk_test_type']]
1200 })
1201 if self.is_long_text:
1202 sso = gmTools.u_superscript_one
1203 else:
1204 sso = ''
1205 tt += ' ' + _('%(sso)sResult: %(val)s%(unit)s%(ind)s [#%(pk_result)s]\n') % ({
1206 'sso': sso,
1207 'val': self._payload[self._idx['unified_val']],
1208 'unit': gmTools.coalesce(self._payload[self._idx['val_unit']], '', ' %s'),
1209 'ind': gmTools.coalesce(self._payload[self._idx['abnormality_indicator']], '', ' (%s)'),
1210 'pk_result': self._payload[self._idx['pk_test_result']]
1211 })
1212
1213 if self._payload[self._idx['status']] is not None:
1214 try:
1215 stat = HL7_RESULT_STATI[self._payload[self._idx['status']]]
1216 except KeyError:
1217 stat = self._payload[self._idx['status']]
1218 tt += ' ' + _('Status: %s\n') % stat
1219 if self._payload[self._idx['val_grouping']] is not None:
1220 tt += ' ' + _('Grouping: %s\n') % self._payload[self._idx['val_grouping']]
1221
1222 if with_evaluation:
1223 norm_eval = None
1224 if self._payload[self._idx['val_num']] is not None:
1225 # 1) normal range
1226 # lowered ?
1227 if (self._payload[self._idx['val_normal_min']] is not None) and (self._payload[self._idx['val_num']] < self._payload[self._idx['val_normal_min']]):
1228 try:
1229 percent = (self._payload[self._idx['val_num']] * 100) / self._payload[self._idx['val_normal_min']]
1230 except ZeroDivisionError:
1231 percent = None
1232 if percent is not None:
1233 if percent < 6:
1234 norm_eval = _('%.1f %% of the normal lower limit') % percent
1235 else:
1236 norm_eval = _('%.0f %% of the normal lower limit') % percent
1237 # raised ?
1238 if (self._payload[self._idx['val_normal_max']] is not None) and (self._payload[self._idx['val_num']] > self._payload[self._idx['val_normal_max']]):
1239 try:
1240 x_times = self._payload[self._idx['val_num']] / self._payload[self._idx['val_normal_max']]
1241 except ZeroDivisionError:
1242 x_times = None
1243 if x_times is not None:
1244 if x_times < 10:
1245 norm_eval = _('%.1f times the normal upper limit') % x_times
1246 else:
1247 norm_eval = _('%.0f times the normal upper limit') % x_times
1248 if norm_eval is not None:
1249 tt += ' = %s\n' % norm_eval
1250 # #-------------------------------------
1251 # # this idea was shot down on the list
1252 # #-------------------------------------
1253 # # bandwidth of deviation
1254 # if None not in [self._payload[self._idx['val_normal_min']], self._payload[self._idx['val_normal_max']]]:
1255 # normal_width = self._payload[self._idx['val_normal_max']] - self._payload[self._idx['val_normal_min']]
1256 # deviation_from_normal_range = None
1257 # # below ?
1258 # if self._payload[self._idx['val_num']] < self._payload[self._idx['val_normal_min']]:
1259 # deviation_from_normal_range = self._payload[self._idx['val_normal_min']] - self._payload[self._idx['val_num']]
1260 # # above ?
1261 # elif self._payload[self._idx['val_num']] > self._payload[self._idx['val_normal_max']]:
1262 # deviation_from_normal_range = self._payload[self._idx['val_num']] - self._payload[self._idx['val_normal_max']]
1263 # if deviation_from_normal_range is None:
1264 # try:
1265 # times_deviation = deviation_from_normal_range / normal_width
1266 # except ZeroDivisionError:
1267 # times_deviation = None
1268 # if times_deviation is not None:
1269 # if times_deviation < 10:
1270 # tt += u' (%s)\n' % _(u'deviates by %.1f times of the normal range') % times_deviation
1271 # else:
1272 # tt += u' (%s)\n' % _(u'deviates by %.0f times of the normal range') % times_deviation
1273 # #-------------------------------------
1274
1275 # 2) clinical target range
1276 norm_eval = None
1277 # lowered ?
1278 if (self._payload[self._idx['val_target_min']] is not None) and (self._payload[self._idx['val_num']] < self._payload[self._idx['val_target_min']]):
1279 try:
1280 percent = (self._payload[self._idx['val_num']] * 100) / self._payload[self._idx['val_target_min']]
1281 except ZeroDivisionError:
1282 percent = None
1283 if percent is not None:
1284 if percent < 6:
1285 norm_eval = _('%.1f %% of the target lower limit') % percent
1286 else:
1287 norm_eval = _('%.0f %% of the target lower limit') % percent
1288 # raised ?
1289 if (self._payload[self._idx['val_target_max']] is not None) and (self._payload[self._idx['val_num']] > self._payload[self._idx['val_target_max']]):
1290 try:
1291 x_times = self._payload[self._idx['val_num']] / self._payload[self._idx['val_target_max']]
1292 except ZeroDivisionError:
1293 x_times = None
1294 if x_times is not None:
1295 if x_times < 10:
1296 norm_eval = _('%.1f times the target upper limit') % x_times
1297 else:
1298 norm_eval = _('%.0f times the target upper limit') % x_times
1299 if norm_eval is not None:
1300 tt += ' = %s\n' % norm_eval
1301 # #-------------------------------------
1302 # # this idea was shot down on the list
1303 # #-------------------------------------
1304 # # bandwidth of deviation
1305 # if None not in [self._payload[self._idx['val_target_min']], self._payload[self._idx['val_target_max']]]:
1306 # normal_width = self._payload[self._idx['val_target_max']] - self._payload[self._idx['val_target_min']]
1307 # deviation_from_target_range = None
1308 # # below ?
1309 # if self._payload[self._idx['val_num']] < self._payload[self._idx['val_target_min']]:
1310 # deviation_from_target_range = self._payload[self._idx['val_target_min']] - self._payload[self._idx['val_num']]
1311 # # above ?
1312 # elif self._payload[self._idx['val_num']] > self._payload[self._idx['val_target_max']]:
1313 # deviation_from_target_range = self._payload[self._idx['val_num']] - self._payload[self._idx['val_target_max']]
1314 # if deviation_from_target_range is None:
1315 # try:
1316 # times_deviation = deviation_from_target_range / normal_width
1317 # except ZeroDivisionError:
1318 # times_deviation = None
1319 # if times_deviation is not None:
1320 # if times_deviation < 10:
1321 # tt += u' (%s)\n' % _(u'deviates by %.1f times of the target range') % times_deviation
1322 # else:
1323 # tt += u' (%s)\n' % _(u'deviates by %.0f times of the target range') % times_deviation
1324 # #-------------------------------------
1325
1326 tmp = ('%s%s' % (
1327 gmTools.coalesce(self._payload[self._idx['name_test_org']], ''),
1328 gmTools.coalesce(self._payload[self._idx['contact_test_org']], '', ' (%s)'),
1329 )).strip()
1330 if tmp != '':
1331 tt += ' ' + _('Source: %s\n') % tmp
1332 tt += '\n'
1333 if self._payload[self._idx['note_test_org']] is not None:
1334 tt += ' ' + gmTools.u_superscript_one + _('Lab comment: %s\n') % _('\n Lab comment: ').join(self._payload[self._idx['note_test_org']].split('\n'))
1335 if self._payload[self._idx['comment']] is not None:
1336 tt += ' ' + gmTools.u_superscript_one + _('Praxis comment: %s\n') % _('\n Praxis comment: ').join(self._payload[self._idx['comment']].split('\n'))
1337
1338 if with_ranges:
1339 tt += gmTools.coalesce(self.formatted_normal_range, '', ' ' + _('Standard normal range: %s\n'))
1340 tt += gmTools.coalesce(self.formatted_clinical_range, '', ' ' + _('Clinical target range: %s\n'))
1341 tt += gmTools.coalesce(self._payload[self._idx['norm_ref_group']], '', ' ' + _('Reference group: %s\n'))
1342
1343 # metadata
1344 if with_episode:
1345 tt += ' ' + _('Episode: %s\n') % self._payload[self._idx['episode']]
1346 if self._payload[self._idx['health_issue']] is not None:
1347 tt += ' ' + _('Issue: %s\n') % self._payload[self._idx['health_issue']]
1348 if self._payload[self._idx['material']] is not None:
1349 tt += ' ' + _('Material: %s\n') % self._payload[self._idx['material']]
1350 if self._payload[self._idx['material_detail']] is not None:
1351 tt += ' ' + _('Details: %s\n') % self._payload[self._idx['material_detail']]
1352 tt += '\n'
1353
1354 if with_review:
1355 if self._payload[self._idx['reviewed']]:
1356 review = gmDateTime.pydt_strftime (
1357 self._payload[self._idx['last_reviewed']],
1358 date_format
1359 )
1360 else:
1361 review = _('not yet')
1362 tt += _('Signed (%(sig_hand)s): %(reviewed)s\n') % ({
1363 'sig_hand': gmTools.u_writing_hand,
1364 'reviewed': review
1365 })
1366 tt += ' ' + _('Responsible clinician: %s\n') % gmTools.bool2subst (
1367 self._payload[self._idx['you_are_responsible']],
1368 _('you'),
1369 self._payload[self._idx['responsible_reviewer']]
1370 )
1371 if self._payload[self._idx['reviewed']]:
1372 tt += ' ' + _('Last reviewer: %(reviewer)s\n') % ({
1373 'reviewer': gmTools.bool2subst (
1374 self._payload[self._idx['review_by_you']],
1375 _('you'),
1376 gmTools.coalesce(self._payload[self._idx['last_reviewer']], '?')
1377 )
1378 })
1379 tt += ' ' + _(' Technically abnormal: %(abnormal)s\n') % ({
1380 'abnormal': gmTools.bool2subst (
1381 self._payload[self._idx['is_technically_abnormal']],
1382 _('yes'),
1383 _('no'),
1384 '?'
1385 )
1386 })
1387 tt += ' ' + _(' Clinically relevant: %(relevant)s\n') % ({
1388 'relevant': gmTools.bool2subst (
1389 self._payload[self._idx['is_clinically_relevant']],
1390 _('yes'),
1391 _('no'),
1392 '?'
1393 )
1394 })
1395 if self._payload[self._idx['review_comment']] is not None:
1396 tt += ' ' + _(' Comment: %s\n') % self._payload[self._idx['review_comment']].strip()
1397 tt += '\n'
1398
1399 # type
1400 if with_type_details:
1401 has_details = None not in [self._payload[self._idx['comment_tt']], self._payload[self._idx['pk_meta_test_type']], self._payload[self._idx['comment_meta']]]
1402 if has_details:
1403 tt += _('Test type details:\n')
1404 if self._payload[self._idx['comment_tt']] is not None:
1405 tt += ' ' + _('Type comment: %s\n') % _('\n Type comment:').join(self._payload[self._idx['comment_tt']].split('\n'))
1406 if self._payload[self._idx['pk_meta_test_type']] is not None:
1407 tt += ' ' + _('Aggregated (%s) under: %s (%s) [#%s]\n') % (
1408 gmTools.u_sum,
1409 self._payload[self._idx['name_meta']],
1410 self._payload[self._idx['abbrev_meta']],
1411 self._payload[self._idx['pk_meta_test_type']]
1412 )
1413 if self._payload[self._idx['comment_meta']] is not None:
1414 tt += ' ' + _('Group comment: %s\n') % _('\n Group comment: ').join(self._payload[self._idx['comment_meta']].split('\n'))
1415 if has_details:
1416 tt += '\n'
1417
1418 if with_source_data:
1419 if self._payload[self._idx['source_data']] is not None:
1420 tt += _('Source data:\n')
1421 tt += ' ' + self._payload[self._idx['source_data']]
1422 tt += '\n\n'
1423
1424 if with_review:
1425 tt += _('Revisions: %(row_ver)s, last %(mod_when)s by %(mod_by)s.') % ({
1426 'row_ver': self._payload[self._idx['row_version']],
1427 'mod_when': gmDateTime.pydt_strftime(self._payload[self._idx['modified_when']],date_format),
1428 'mod_by': self._payload[self._idx['modified_by']]
1429 })
1430
1431 return tt
1432
1433 #--------------------------------------------------------
1435 return (
1436 self._payload[self._idx['val_normal_min']] is not None
1437 ) or (
1438 self._payload[self._idx['val_normal_max']] is not None
1439 )
1440
1441 has_normal_min_or_max = property(_get_has_normal_min_or_max, lambda x:x)
1442
1443 #--------------------------------------------------------
1445 has_range_info = (
1446 self._payload[self._idx['val_normal_min']] is not None
1447 ) or (
1448 self._payload[self._idx['val_normal_max']] is not None
1449 )
1450 if has_range_info is False:
1451 return None
1452
1453 return '%s - %s' % (
1454 gmTools.coalesce(self._payload[self._idx['val_normal_min']], '?'),
1455 gmTools.coalesce(self._payload[self._idx['val_normal_max']], '?')
1456 )
1457
1458 normal_min_max = property(_get_normal_min_max, lambda x:x)
1459
1460 #--------------------------------------------------------
1462 has_numerical_range = (
1463 self._payload[self._idx['val_normal_min']] is not None
1464 ) or (
1465 self._payload[self._idx['val_normal_max']] is not None
1466 )
1467 if has_numerical_range:
1468 numerical_range = '%s - %s' % (
1469 gmTools.coalesce(self._payload[self._idx['val_normal_min']], '?'),
1470 gmTools.coalesce(self._payload[self._idx['val_normal_max']], '?')
1471 )
1472 else:
1473 numerical_range = ''
1474 textual_range = gmTools.coalesce (
1475 self._payload[self._idx['val_normal_range']],
1476 '',
1477 gmTools.bool2subst (
1478 has_numerical_range,
1479 ' / %s',
1480 '%s'
1481 )
1482 )
1483 range_info = '%s%s' % (numerical_range, textual_range)
1484 if range_info == '':
1485 return None
1486 return range_info
1487
1488 formatted_normal_range = property(_get_formatted_normal_range, lambda x:x)
1489
1490 #--------------------------------------------------------
1492 return (
1493 self._payload[self._idx['val_target_min']] is not None
1494 ) or (
1495 self._payload[self._idx['val_target_max']] is not None
1496 )
1497
1498 has_clinical_min_or_max = property(_get_has_clinical_min_or_max, lambda x:x)
1499
1500 #--------------------------------------------------------
1502 has_range_info = (
1503 self._payload[self._idx['val_target_min']] is not None
1504 ) or (
1505 self._payload[self._idx['val_target_max']] is not None
1506 )
1507 if has_range_info is False:
1508 return None
1509
1510 return '%s - %s' % (
1511 gmTools.coalesce(self._payload[self._idx['val_target_min']], '?'),
1512 gmTools.coalesce(self._payload[self._idx['val_target_max']], '?')
1513 )
1514
1515 clinical_min_max = property(_get_clinical_min_max, lambda x:x)
1516
1517 #--------------------------------------------------------
1519 has_numerical_range = (
1520 self._payload[self._idx['val_target_min']] is not None
1521 ) or (
1522 self._payload[self._idx['val_target_max']] is not None
1523 )
1524 if has_numerical_range:
1525 numerical_range = '%s - %s' % (
1526 gmTools.coalesce(self._payload[self._idx['val_target_min']], '?'),
1527 gmTools.coalesce(self._payload[self._idx['val_target_max']], '?')
1528 )
1529 else:
1530 numerical_range = ''
1531 textual_range = gmTools.coalesce (
1532 self._payload[self._idx['val_target_range']],
1533 '',
1534 gmTools.bool2subst (
1535 has_numerical_range,
1536 ' / %s',
1537 '%s'
1538 )
1539 )
1540 range_info = '%s%s' % (numerical_range, textual_range)
1541 if range_info == '':
1542 return None
1543 return range_info
1544
1545 formatted_clinical_range = property(_get_formatted_clinical_range, lambda x:x)
1546
1547 #--------------------------------------------------------
1549 """Returns the closest test result which does have normal range information."""
1550 if self._payload[self._idx['val_normal_min']] is not None:
1551 return self
1552 if self._payload[self._idx['val_normal_max']] is not None:
1553 return self
1554 if self._payload[self._idx['val_normal_range']] is not None:
1555 return self
1556 cmd = """
1557 SELECT * from clin.v_test_results
1558 WHERE
1559 pk_type = %(pk_type)s
1560 AND
1561 val_unit = %(unit)s
1562 AND
1563 (
1564 (val_normal_min IS NOT NULL)
1565 OR
1566 (val_normal_max IS NOT NULL)
1567 OR
1568 (val_normal_range IS NOT NULL)
1569 )
1570 ORDER BY
1571 CASE
1572 WHEN clin_when > %(clin_when)s THEN clin_when - %(clin_when)s
1573 ELSE %(clin_when)s - clin_when
1574 END
1575 LIMIT 1"""
1576 args = {
1577 'pk_type': self._payload[self._idx['pk_test_type']],
1578 'unit': self._payload[self._idx['val_unit']],
1579 'clin_when': self._payload[self._idx['clin_when']]
1580 }
1581 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
1582 if len(rows) == 0:
1583 return None
1584 return cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': rows[0]})
1585
1586 temporally_closest_normal_range = property(_get_temporally_closest_normal_range, lambda x:x)
1587
1588 #--------------------------------------------------------
1590
1591 has_normal_min_or_max = (
1592 self._payload[self._idx['val_normal_min']] is not None
1593 ) or (
1594 self._payload[self._idx['val_normal_max']] is not None
1595 )
1596 if has_normal_min_or_max:
1597 normal_min_max = '%s - %s' % (
1598 gmTools.coalesce(self._payload[self._idx['val_normal_min']], '?'),
1599 gmTools.coalesce(self._payload[self._idx['val_normal_max']], '?')
1600 )
1601
1602 has_clinical_min_or_max = (
1603 self._payload[self._idx['val_target_min']] is not None
1604 ) or (
1605 self._payload[self._idx['val_target_max']] is not None
1606 )
1607 if has_clinical_min_or_max:
1608 clinical_min_max = '%s - %s' % (
1609 gmTools.coalesce(self._payload[self._idx['val_target_min']], '?'),
1610 gmTools.coalesce(self._payload[self._idx['val_target_max']], '?')
1611 )
1612
1613 if has_clinical_min_or_max:
1614 return _('Target: %(clin_min_max)s%(clin_range)s') % ({
1615 'clin_min_max': clinical_min_max,
1616 'clin_range': gmTools.coalesce (
1617 self._payload[self._idx['val_target_range']],
1618 '',
1619 gmTools.bool2subst (
1620 has_clinical_min_or_max,
1621 ' / %s',
1622 '%s'
1623 )
1624 )
1625 })
1626
1627 if has_normal_min_or_max:
1628 return _('Norm: %(norm_min_max)s%(norm_range)s') % ({
1629 'norm_min_max': normal_min_max,
1630 'norm_range': gmTools.coalesce (
1631 self._payload[self._idx['val_normal_range']],
1632 '',
1633 gmTools.bool2subst (
1634 has_normal_min_or_max,
1635 ' / %s',
1636 '%s'
1637 )
1638 )
1639 })
1640
1641 if self._payload[self._idx['val_target_range']] is not None:
1642 return _('Target: %s') % self._payload[self._idx['val_target_range']],
1643
1644 if self._payload[self._idx['val_normal_range']] is not None:
1645 return _('Norm: %s') % self._payload[self._idx['val_normal_range']]
1646
1647 return None
1648
1649 formatted_range = property(_get_formatted_range, lambda x:x)
1650
1651 #--------------------------------------------------------
1653 return cMeasurementType(aPK_obj = self._payload[self._idx['pk_test_type']])
1654
1655 test_type = property(_get_test_type, lambda x:x)
1656
1657 #--------------------------------------------------------
1659 # 1) the user is right (review)
1660 if self._payload[self._idx['is_technically_abnormal']] is False:
1661 return False
1662 # 2) the lab is right (result.abnormality_indicator)
1663 indicator = self._payload[self._idx['abnormality_indicator']]
1664 if indicator is not None:
1665 indicator = indicator.strip()
1666 if indicator != '':
1667 if indicator.strip('+') == '':
1668 return True
1669 if indicator.strip('-') == '':
1670 return False
1671 # 3) non-numerical value ?
1672 if self._payload[self._idx['val_num']] is None:
1673 return None
1674 # 4) the target range is right
1675 target_max = self._payload[self._idx['val_target_max']]
1676 if target_max is not None:
1677 if target_max < self._payload[self._idx['val_num']]:
1678 return True
1679 # 4) the normal range is right
1680 normal_max = self._payload[self._idx['val_normal_max']]
1681 if normal_max is not None:
1682 if normal_max < self._payload[self._idx['val_num']]:
1683 return True
1684 return None
1685
1686 is_considered_elevated = property(_get_is_considered_elevated, lambda x:x)
1687
1688 #--------------------------------------------------------
1690 # 1) the user is right (review)
1691 if self._payload[self._idx['is_technically_abnormal']] is False:
1692 return False
1693 # 2) the lab is right (result.abnormality_indicator)
1694 indicator = self._payload[self._idx['abnormality_indicator']]
1695 if indicator is not None:
1696 indicator = indicator.strip()
1697 if indicator != '':
1698 if indicator.strip('+') == '':
1699 return False
1700 if indicator.strip('-') == '':
1701 return True
1702 # 3) non-numerical value ?
1703 if self._payload[self._idx['val_num']] is None:
1704 return None
1705 # 4) the target range is right
1706 target_min = self._payload[self._idx['val_target_min']]
1707 if target_min is not None:
1708 if target_min > self._payload[self._idx['val_num']]:
1709 return True
1710 # 4) the normal range is right
1711 normal_min = self._payload[self._idx['val_normal_min']]
1712 if normal_min is not None:
1713 if normal_min > self._payload[self._idx['val_num']]:
1714 return True
1715 return None
1716
1717 is_considered_lowered = property(_get_is_considered_lowered, lambda x:x)
1718
1719 #--------------------------------------------------------
1721 if self.is_considered_lowered is True:
1722 return True
1723 if self.is_considered_elevated is True:
1724 return True
1725 if (self.is_considered_lowered is False) and (self.is_considered_elevated is False):
1726 return False
1727 return self._payload[self._idx['is_technically_abnormal']]
1728
1729 is_considered_abnormal = property(_get_is_considered_abnormal, lambda x:x)
1730
1731 #--------------------------------------------------------
1733 """Parse reference range from string.
1734
1735 Note: does NOT save the result.
1736 """
1737 ref_range = ref_range.strip().replace(' ', '')
1738
1739 is_range = regex.match('-{0,1}\d+[.,]{0,1}\d*--{0,1}\d+[.,]{0,1}\d*$', ref_range, regex.UNICODE)
1740 if is_range is not None:
1741 min_val = regex.match('-{0,1}\d+[.,]{0,1}\d*-', ref_range, regex.UNICODE).group(0).rstrip('-')
1742 success, min_val = gmTools.input2decimal(min_val)
1743 max_val = (regex.search('--{0,1}\d+[.,]{0,1}\d*$', ref_range, regex.UNICODE).group(0))[1:]
1744 success, max_val = gmTools.input2decimal(max_val)
1745 self['val_normal_min'] = min_val
1746 self['val_normal_max'] = max_val
1747 return
1748
1749 if ref_range.startswith('<'):
1750 is_range = regex.match('<\d+[.,]{0,1}\d*$', ref_range, regex.UNICODE)
1751 if is_range is not None:
1752 max_val = ref_range[1:]
1753 success, max_val = gmTools.input2decimal(max_val)
1754 self['val_normal_min'] = 0
1755 self['val_normal_max'] = max_val
1756 return
1757
1758 if ref_range.startswith('<-'):
1759 is_range = regex.match('<-\d+[.,]{0,1}\d*$', ref_range, regex.UNICODE)
1760 if is_range is not None:
1761 max_val = ref_range[1:]
1762 success, max_val = gmTools.input2decimal(max_val)
1763 self['val_normal_min'] = None
1764 self['val_normal_max'] = max_val
1765 return
1766
1767 if ref_range.startswith('>'):
1768 is_range = regex.match('>\d+[.,]{0,1}\d*$', ref_range, regex.UNICODE)
1769 if is_range is not None:
1770 min_val = ref_range[1:]
1771 success, min_val = gmTools.input2decimal(min_val)
1772 self['val_normal_min'] = min_val
1773 self['val_normal_max'] = None
1774 return
1775
1776 if ref_range.startswith('>-'):
1777 is_range = regex.match('>-\d+[.,]{0,1}\d*$', ref_range, regex.UNICODE)
1778 if is_range is not None:
1779 min_val = ref_range[1:]
1780 success, min_val = gmTools.input2decimal(min_val)
1781 self['val_normal_min'] = min_val
1782 self['val_normal_max'] = 0
1783 return
1784
1785 self['val_normal_range'] = ref_range
1786 return
1787
1788 reference_range = property(lambda x:x, _set_reference_range)
1789
1790 #--------------------------------------------------------
1792 # 1) the user is right
1793 if self._payload[self._idx['is_technically_abnormal']] is False:
1794 return ''
1795 # 2) the lab is right (result.abnormality_indicator)
1796 indicator = self._payload[self._idx['abnormality_indicator']]
1797 if indicator is not None:
1798 indicator = indicator.strip()
1799 if indicator != '':
1800 return indicator
1801 # 3) non-numerical value ? then we can't know more
1802 if self._payload[self._idx['val_num']] is None:
1803 return None
1804 # 4) the target range is right
1805 target_min = self._payload[self._idx['val_target_min']]
1806 if target_min is not None:
1807 if target_min > self._payload[self._idx['val_num']]:
1808 return '-'
1809 target_max = self._payload[self._idx['val_target_max']]
1810 if target_max is not None:
1811 if target_max < self._payload[self._idx['val_num']]:
1812 return '+'
1813 # 4) the normal range is right
1814 normal_min = self._payload[self._idx['val_normal_min']]
1815 if normal_min is not None:
1816 if normal_min > self._payload[self._idx['val_num']]:
1817 return '-'
1818 normal_max = self._payload[self._idx['val_normal_max']]
1819 if normal_max is not None:
1820 if normal_max < self._payload[self._idx['val_num']]:
1821 return '+'
1822 # reviewed, abnormal, but no indicator available
1823 if self._payload[self._idx['is_technically_abnormal']] is True:
1824 return gmTools.u_plus_minus
1825
1826 return None
1827
1828 formatted_abnormality_indicator = property(_get_formatted_abnormality_indicator, lambda x:x)
1829
1830 #--------------------------------------------------------
1832 if self._payload[self._idx['val_alpha']] is None:
1833 return False
1834 lines = gmTools.strip_empty_lines(text = self._payload[self._idx['val_alpha']], eol = '\n', return_list = True)
1835 if len(lines) > 4:
1836 return True
1837 return False
1838
1839 is_long_text = property(_get_is_long_text, lambda x:x)
1840
1841 #--------------------------------------------------------
1843 if self._payload[self._idx['val_alpha']] is None:
1844 return None
1845 val = self._payload[self._idx['val_alpha']].lstrip()
1846 if val[0] == '<':
1847 factor = decimal.Decimal(0.5)
1848 val = val[1:]
1849 elif val[0] == '>':
1850 factor = 2
1851 val = val[1:]
1852 else:
1853 return None
1854 success, val = gmTools.input2decimal(initial = val)
1855 if not success:
1856 return None
1857 return val * factor
1858
1859 estimate_numeric_value_from_alpha = property(_get_estimate_numeric_value_from_alpha, lambda x:x)
1860
1861 #--------------------------------------------------------
1862 - def set_review(self, technically_abnormal=None, clinically_relevant=None, comment=None, make_me_responsible=False):
1863
1864 # FIXME: this is not concurrency safe
1865 if self._payload[self._idx['reviewed']]:
1866 self.__change_existing_review (
1867 technically_abnormal = technically_abnormal,
1868 clinically_relevant = clinically_relevant,
1869 comment = comment
1870 )
1871 else:
1872 # do not sign off unreviewed results if
1873 # NOTHING AT ALL is known about them
1874 if technically_abnormal is None:
1875 if clinically_relevant is None:
1876 comment = gmTools.none_if(comment, '', strip_string = True)
1877 if comment is None:
1878 if make_me_responsible is False:
1879 return True
1880 self.__set_new_review (
1881 technically_abnormal = technically_abnormal,
1882 clinically_relevant = clinically_relevant,
1883 comment = comment
1884 )
1885
1886 if make_me_responsible is True:
1887 cmd = "SELECT pk FROM dem.staff WHERE db_user = current_user"
1888 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd}])
1889 self['pk_intended_reviewer'] = rows[0][0]
1890 self.save_payload()
1891 return
1892
1893 self.refetch_payload()
1894
1895 #--------------------------------------------------------
1896 - def get_adjacent_results(self, desired_earlier_results=1, desired_later_results=1, max_offset=None):
1897
1898 if desired_earlier_results < 1:
1899 raise ValueError('<desired_earlier_results> must be > 0')
1900
1901 if desired_later_results < 1:
1902 raise ValueError('<desired_later_results> must be > 0')
1903
1904 args = {
1905 'pat': self._payload[self._idx['pk_patient']],
1906 'ttyp': self._payload[self._idx['pk_test_type']],
1907 'tloinc': self._payload[self._idx['loinc_tt']],
1908 'mtyp': self._payload[self._idx['pk_meta_test_type']],
1909 'mloinc': self._payload[self._idx['loinc_meta']],
1910 'when': self._payload[self._idx['clin_when']],
1911 'offset': max_offset
1912 }
1913 WHERE = '((pk_test_type = %(ttyp)s) OR (loinc_tt = %(tloinc)s))'
1914 WHERE_meta = '((pk_meta_test_type = %(mtyp)s) OR (loinc_meta = %(mloinc)s))'
1915 if max_offset is not None:
1916 WHERE = WHERE + ' AND (clin_when BETWEEN (%(when)s - %(offset)s) AND (%(when)s + %(offset)s))'
1917 WHERE_meta = WHERE_meta + ' AND (clin_when BETWEEN (%(when)s - %(offset)s) AND (%(when)s + %(offset)s))'
1918
1919 SQL = """
1920 SELECT * FROM clin.v_test_results
1921 WHERE
1922 pk_patient = %%(pat)s
1923 AND
1924 clin_when %s %%(when)s
1925 AND
1926 %s
1927 ORDER BY clin_when
1928 LIMIT %s"""
1929
1930 # get earlier results
1931 earlier_results = []
1932 # by type
1933 cmd = SQL % ('<', WHERE, desired_earlier_results)
1934 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
1935 if len(rows) > 0:
1936 earlier_results.extend([ cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': r}) for r in rows ])
1937 # by meta type ?
1938 missing_results = desired_earlier_results - len(earlier_results)
1939 if missing_results > 0:
1940 cmd = SQL % ('<', WHERE_meta, missing_results)
1941 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
1942 if len(rows) > 0:
1943 earlier_results.extend([ cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': r}) for r in rows ])
1944
1945 # get later results
1946 later_results = []
1947 # by type
1948 cmd = SQL % ('>', WHERE, desired_later_results)
1949 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
1950 if len(rows) > 0:
1951 later_results.extend([ cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': r}) for r in rows ])
1952 # by meta type ?
1953 missing_results = desired_later_results - len(later_results)
1954 if missing_results > 0:
1955 cmd = SQL % ('>', WHERE_meta, missing_results)
1956 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
1957 if len(rows) > 0:
1958 later_results.extend([ cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': r}) for r in rows ])
1959
1960 return earlier_results, later_results
1961
1962 #--------------------------------------------------------
1963 # internal API
1964 #--------------------------------------------------------
1965 - def __set_new_review(self, technically_abnormal=None, clinically_relevant=None, comment=None):
1966 """Add a review to a row.
1967
1968 - if technically abnormal is not provided/None it will be set
1969 to True if the lab's indicator has a meaningful value
1970 - if clinically relevant is not provided/None it is set to
1971 whatever technically abnormal is
1972 """
1973 if technically_abnormal is None:
1974 technically_abnormal = False
1975 if self._payload[self._idx['abnormality_indicator']] is not None:
1976 if self._payload[self._idx['abnormality_indicator']].strip() != '':
1977 technically_abnormal = True
1978
1979 if clinically_relevant is None:
1980 clinically_relevant = technically_abnormal
1981
1982 cmd = """
1983 INSERT INTO clin.reviewed_test_results (
1984 fk_reviewed_row,
1985 is_technically_abnormal,
1986 clinically_relevant,
1987 comment
1988 ) VALUES (
1989 %(pk)s,
1990 %(abnormal)s,
1991 %(relevant)s,
1992 gm.nullify_empty_string(%(cmt)s)
1993 )"""
1994 args = {
1995 'pk': self._payload[self._idx['pk_test_result']],
1996 'abnormal': technically_abnormal,
1997 'relevant': clinically_relevant,
1998 'cmt': comment
1999 }
2000
2001 gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
2002
2003 #--------------------------------------------------------
2004 - def __change_existing_review(self, technically_abnormal=None, clinically_relevant=None, comment=None):
2005 """Change a review on a row.
2006
2007 - if technically abnormal/clinically relevant are
2008 None they are not set
2009 """
2010 args = {
2011 'pk_result': self._payload[self._idx['pk_test_result']],
2012 'abnormal': technically_abnormal,
2013 'relevant': clinically_relevant,
2014 'cmt': comment
2015 }
2016
2017 set_parts = [
2018 'fk_reviewer = (SELECT pk FROM dem.staff WHERE db_user = current_user)',
2019 'comment = gm.nullify_empty_string(%(cmt)s)'
2020 ]
2021
2022 if technically_abnormal is not None:
2023 set_parts.append('is_technically_abnormal = %(abnormal)s')
2024
2025 if clinically_relevant is not None:
2026 set_parts.append('clinically_relevant = %(relevant)s')
2027
2028 cmd = """
2029 UPDATE clin.reviewed_test_results SET
2030 %s
2031 WHERE
2032 fk_reviewed_row = %%(pk_result)s
2033 """ % ',\n '.join(set_parts)
2034
2035 gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
2036
2037 #------------------------------------------------------------
2038 -def get_test_results(pk_patient=None, encounters=None, episodes=None, order_by=None, return_pks=False):
2039
2040 where_parts = []
2041
2042 if pk_patient is not None:
2043 where_parts.append('pk_patient = %(pat)s')
2044 args = {'pat': pk_patient}
2045
2046 # if tests is not None:
2047 # where_parts.append(u'pk_test_type IN %(tests)s')
2048 # args['tests'] = tuple(tests)
2049
2050 if encounters is not None:
2051 where_parts.append('pk_encounter IN %(encs)s')
2052 args['encs'] = tuple(encounters)
2053
2054 if episodes is not None:
2055 where_parts.append('pk_episode IN %(epis)s')
2056 args['epis'] = tuple(episodes)
2057
2058 if order_by is None:
2059 order_by = ''
2060 else:
2061 order_by = 'ORDER BY %s' % order_by
2062
2063 cmd = """
2064 SELECT * FROM clin.v_test_results
2065 WHERE %s
2066 %s
2067 """ % (
2068 ' AND '.join(where_parts),
2069 order_by
2070 )
2071 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
2072 if return_pks:
2073 return [ r['pk_test_result'] for r in rows ]
2074 tests = [ cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': r}) for r in rows ]
2075 return tests
2076
2077 #------------------------------------------------------------
2078 -def get_most_recent_results_for_panel(pk_patient=None, pk_panel=None, order_by=None, group_by_meta_type=False):
2079
2080 if order_by is None:
2081 order_by = ''
2082 else:
2083 order_by = 'ORDER BY %s' % order_by
2084
2085 args = {
2086 'pat': pk_patient,
2087 'pk_pnl': pk_panel
2088 }
2089
2090 if group_by_meta_type:
2091 # return most recent results in panel grouped by
2092 # meta test type if any, non-grouped results are
2093 # returned ungrouped :-)
2094 cmd = """
2095 SELECT c_vtr.*
2096 FROM (
2097 -- max(clin_when) per test_type-in-panel for patient
2098 SELECT
2099 pk_meta_test_type,
2100 MAX(clin_when) AS max_clin_when
2101 FROM clin.v_test_results
2102 WHERE
2103 pk_patient = %(pat)s
2104 AND
2105 pk_meta_test_type IS DISTINCT FROM NULL
2106 AND
2107 pk_test_type IN (
2108 (SELECT c_vtt4tp.pk_test_type FROM clin.v_test_types4test_panel c_vtt4tp WHERE c_vtt4tp.pk_test_panel = %(pk_pnl)s)
2109 )
2110 GROUP BY pk_meta_test_type
2111 ) AS latest_results
2112 INNER JOIN clin.v_test_results c_vtr ON
2113 c_vtr.pk_meta_test_type = latest_results.pk_meta_test_type
2114 AND
2115 c_vtr.clin_when = latest_results.max_clin_when
2116
2117 UNION ALL
2118
2119 SELECT c_vtr.*
2120 FROM (
2121 -- max(clin_when) per test_type-in-panel for patient
2122 SELECT
2123 pk_test_type,
2124 MAX(clin_when) AS max_clin_when
2125 FROM clin.v_test_results
2126 WHERE
2127 pk_patient = %(pat)s
2128 AND
2129 pk_meta_test_type IS NULL
2130 AND
2131 pk_test_type IN (
2132 (SELECT c_vtt4tp.pk_test_type FROM clin.v_test_types4test_panel c_vtt4tp WHERE c_vtt4tp.pk_test_panel = %(pk_pnl)s)
2133 )
2134 GROUP BY pk_test_type
2135 ) AS latest_results
2136 INNER JOIN clin.v_test_results c_vtr ON
2137 c_vtr.pk_test_type = latest_results.pk_test_type
2138 AND
2139 c_vtr.clin_when = latest_results.max_clin_when
2140 """
2141 else:
2142 # return most recent results in panel regardless of whether
2143 # distinct test types in this panel are grouped under the
2144 # same meta test type
2145 cmd = """
2146 SELECT c_vtr.*
2147 FROM (
2148 -- max(clin_when) per test_type-in-panel for patient
2149 SELECT
2150 pk_test_type,
2151 MAX(clin_when) AS max_clin_when
2152 FROM clin.v_test_results
2153 WHERE
2154 pk_patient = %(pat)s
2155 AND
2156 pk_test_type IN (
2157 (SELECT c_vtt4tp.pk_test_type FROM clin.v_test_types4test_panel c_vtt4tp WHERE c_vtt4tp.pk_test_panel = %(pk_pnl)s)
2158 )
2159 GROUP BY pk_test_type
2160 ) AS latest_results
2161 -- this INNER join makes certain we do not expand
2162 -- the row selection beyond the patient's rows
2163 -- which we constrained to inside the SELECT
2164 -- producing "latest_results"
2165 INNER JOIN clin.v_test_results c_vtr ON
2166 c_vtr.pk_test_type = latest_results.pk_test_type
2167 AND
2168 c_vtr.clin_when = latest_results.max_clin_when
2169 """
2170 cmd += order_by
2171 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
2172 return [ cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': r}) for r in rows ]
2173
2174 #------------------------------------------------------------
2175 -def get_result_at_timestamp(timestamp=None, test_type=None, loinc=None, tolerance_interval=None, patient=None):
2176
2177 if None not in [test_type, loinc]:
2178 raise ValueError('either <test_type> or <loinc> must be None')
2179
2180 args = {
2181 'pat': patient,
2182 'ttyp': test_type,
2183 'loinc': loinc,
2184 'ts': timestamp,
2185 'intv': tolerance_interval
2186 }
2187
2188 where_parts = ['pk_patient = %(pat)s']
2189 if test_type is not None:
2190 where_parts.append('pk_test_type = %(ttyp)s') # consider: pk_meta_test_type = %(pkmtt)s / self._payload[self._idx['pk_meta_test_type']]
2191 elif loinc is not None:
2192 where_parts.append('((loinc_tt IN %(loinc)s) OR (loinc_meta IN %(loinc)s))')
2193 args['loinc'] = tuple(loinc)
2194
2195 if tolerance_interval is None:
2196 where_parts.append('clin_when = %(ts)s')
2197 else:
2198 where_parts.append('clin_when between (%(ts)s - %(intv)s::interval) AND (%(ts)s + %(intv)s::interval)')
2199
2200 cmd = """
2201 SELECT * FROM clin.v_test_results
2202 WHERE
2203 %s
2204 ORDER BY
2205 abs(extract(epoch from age(clin_when, %%(ts)s)))
2206 LIMIT 1""" % ' AND '.join(where_parts)
2207
2208 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
2209 if len(rows) == 0:
2210 return None
2211
2212 return cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': rows[0]})
2213
2214 #------------------------------------------------------------
2216
2217 args = {
2218 'pat': patient,
2219 'ts': timestamp
2220 }
2221
2222 where_parts = [
2223 'pk_patient = %(pat)s',
2224 "date_trunc('day'::text, clin_when) = date_trunc('day'::text, %(ts)s)"
2225 ]
2226
2227 cmd = """
2228 SELECT * FROM clin.v_test_results
2229 WHERE
2230 %s
2231 ORDER BY
2232 val_grouping,
2233 abbrev_tt,
2234 clin_when DESC
2235 """ % ' AND '.join(where_parts)
2236 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
2237 return [ cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': r}) for r in rows ]
2238
2239 #------------------------------------------------------------
2241 args = {'pk_issue': pk_health_issue}
2242 where_parts = ['pk_health_issue = %(pk_issue)s']
2243 cmd = """
2244 SELECT * FROM clin.v_test_results
2245 WHERE %s
2246 ORDER BY
2247 val_grouping,
2248 abbrev_tt,
2249 clin_when DESC
2250 """ % ' AND '.join(where_parts)
2251 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
2252 return [ cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': r}) for r in rows ]
2253
2254 #------------------------------------------------------------
2256 args = {'pk_epi': pk_episode}
2257 where_parts = ['pk_episode = %(pk_epi)s']
2258 cmd = """
2259 SELECT * FROM clin.v_test_results
2260 WHERE %s
2261 ORDER BY
2262 val_grouping,
2263 abbrev_tt,
2264 clin_when DESC
2265 """ % ' AND '.join(where_parts)
2266 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
2267 return [ cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': r}) for r in rows ]
2268
2269 #------------------------------------------------------------
2270 -def get_most_recent_results_in_loinc_group(loincs=None, max_no_of_results=1, patient=None, max_age=None, consider_indirect_matches=False):
2271 """Get N most recent results *among* a list of tests selected by LOINC.
2272
2273 1) get test types with LOINC (or meta type LOINC) in the group of <loincs>
2274 2) from these get the test results for <patient> within the given <max_age>
2275 3) from these return "the N=<max_no_of_results> most recent ones" or "None"
2276
2277 <loinc> must be a list or tuple or set, NOT a single string
2278 <max_age> must be a string holding a PG interval or else a pydt interval
2279 """
2280 assert (max_no_of_results > 0), '<max_no_of_results> must be >0'
2281
2282 args = {'pat': patient, 'loincs': tuple(loincs)}
2283 if max_age is None:
2284 max_age_cond = ''
2285 else:
2286 max_age_cond = 'AND clin_when > (now() - %(max_age)s::interval)'
2287 args['max_age'] = max_age
2288 cmd = """
2289 SELECT * FROM (
2290 SELECT DISTINCT ON (pk_test_result) *
2291 FROM clin.v_test_results
2292 WHERE
2293 pk_patient = %%(pat)s
2294 AND
2295 unified_loinc IN %%(loincs)s
2296 %s
2297 ) AS distinct_results
2298 ORDER BY
2299 clin_when DESC
2300 LIMIT %s""" % (
2301 max_age_cond,
2302 max_no_of_results
2303 )
2304 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
2305 if len(rows) > 0:
2306 return [ cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': r}) for r in rows ]
2307
2308 if not consider_indirect_matches:
2309 return []
2310
2311 cmd = """
2312 -- get the test results
2313 SELECT * FROM clin.v_test_results c_vtr
2314 WHERE
2315 -- for this <patient>
2316 pk_patient = %%(pat)s
2317 AND
2318 -- not having *any* LOINC (if the result did have a LOINC and had not been caught by the by-LOINC search it does not apply)
2319 unified_loinc IS NULL
2320 AND
2321 -- with these meta test types (= results for the explicit equivalance group)
2322 c_vtr.pk_meta_test_type IN (
2323 -- get the meta test types for those types
2324 SELECT pk_meta_test_type
2325 FROM clin.v_test_types
2326 WHERE
2327 pk_meta_test_type IS NOT NULL
2328 AND
2329 (-- retrieve test types which have .LOINC in <loincs>
2330 (loinc IN %%(loincs)s)
2331 OR
2332 (loinc_meta IN %%(loincs)s)
2333 )
2334 AND
2335 -- but no result for <patient>
2336 pk_test_type NOT IN (
2337 select pk_test_type from clin.v_test_results WHERE pk_patient = %%(pat)s
2338 ) %s
2339 )
2340 -- return the N most resent result
2341 ORDER BY clin_when DESC
2342 LIMIT %s
2343 """ % (
2344 max_age_cond,
2345 max_no_of_results
2346 )
2347 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
2348 return [ cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': r}) for r in rows ]
2349
2350 #------------------------------------------------------------
2352 """Get N most recent results for *one* defined test type."""
2353
2354 assert (test_type is not None), '<test_type> must not be None'
2355 assert (max_no_of_results > 0), '<max_no_of_results> must be > 0'
2356
2357 args = {'pat': patient, 'ttyp': test_type}
2358 where_parts = ['pk_patient = %(pat)s']
2359 where_parts.append('pk_test_type = %(ttyp)s') # ?consider: pk_meta_test_type = %(pkmtt)s / self._payload[self._idx['pk_meta_test_type']]
2360 cmd = """
2361 SELECT * FROM clin.v_test_results
2362 WHERE
2363 %s
2364 ORDER BY clin_when DESC
2365 LIMIT %s""" % (
2366 ' AND '.join(where_parts),
2367 max_no_of_results
2368 )
2369 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
2370 return [ cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': r}) for r in rows ]
2371
2372 #------------------------------------------------------------
2373 _SQL_most_recent_result_for_test_types = """
2374 -- return the one most recent result for each of a list of test types
2375 -- without regard to whether they belong to a meta test type
2376 SELECT * FROM (
2377 SELECT
2378 *,
2379 MAX(clin_when) OVER relevant_tests AS max_clin_when
2380 FROM
2381 clin.v_test_results
2382 WHERE
2383 %s
2384 WINDOW relevant_tests AS (PARTITION BY pk_patient, pk_test_type)
2385 ) AS windowed_tests
2386 WHERE
2387 clin_when = max_clin_when
2388 %s
2389 """
2390
2391 _SQL_most_recent_result_for_test_types_without_meta_type = """
2392 -- return the one most recent result for each of a list of test types
2393 -- none of which may belong to a meta test type
2394 SELECT * FROM (
2395 SELECT
2396 *,
2397 MAX(clin_when) OVER relevant_tests AS max_clin_when
2398 FROM
2399 clin.v_test_results
2400 WHERE
2401 pk_meta_test_type IS NULL
2402 AND
2403 %s
2404 WINDOW relevant_tests AS (PARTITION BY pk_patient, pk_test_type)
2405 ) AS windowed_tests
2406 WHERE
2407 clin_when = max_clin_when
2408 """
2409
2410 _SQL_most_recent_result_for_test_types_by_meta_type = """
2411 -- return the one most recent result for each of a list of meta test types
2412 -- derived from a list of test types
2413 SELECT * FROM (
2414 SELECT
2415 *,
2416 MAX(clin_when) OVER relevant_tests AS max_clin_when
2417 FROM
2418 clin.v_test_results
2419 WHERE
2420 pk_meta_test_type IS NOT NULL
2421 AND
2422 %s
2423 WINDOW relevant_tests AS (PARTITION BY pk_patient, pk_meta_test_type)
2424 ) AS windowed_tests
2425 WHERE
2426 clin_when = max_clin_when
2427 """
2428
2429 -def get_most_recent_result_for_test_types(pk_test_types=None, pk_patient=None, return_pks=False, consider_meta_type=False, order_by=None):
2430 """Return the one most recent result for *each* of a list of test types.
2431
2432 If <pk_test_types> is not given, most recent results for *each*
2433 test type the patient has any results for is returned.
2434 """
2435 where_parts = ['pk_patient = %(pat)s']
2436 args = {'pat': pk_patient}
2437
2438 if pk_test_types is not None:
2439 where_parts.append('pk_test_type IN %(ttyps)s')
2440 args['ttyps'] = tuple(pk_test_types)
2441
2442 order_by = 'ORDER BY clin_when DESC' if order_by is None else 'ORDER BY %s' % order_by
2443
2444 if consider_meta_type:
2445 cmd = 'SELECT * FROM ((%s) UNION ALL (%s)) AS result_union %s' % (
2446 _SQL_most_recent_result_for_test_types_without_meta_type % ' AND '.join(where_parts),
2447 _SQL_most_recent_result_for_test_types_by_meta_type % ' AND '.join(where_parts),
2448 order_by
2449 )
2450 else:
2451 cmd = _SQL_most_recent_result_for_test_types % (
2452 ' AND '.join(where_parts),
2453 order_by
2454 )
2455 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
2456 if return_pks:
2457 return [ r['pk_test_result'] for r in rows ]
2458
2459 return [ cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': r}) for r in rows ]
2460
2461 #------------------------------------------------------------
2463 """Get N most recent results for a given patient."""
2464
2465 if no_of_results < 1:
2466 raise ValueError('<no_of_results> must be > 0')
2467
2468 args = {'pat': patient}
2469 cmd = """
2470 SELECT * FROM clin.v_test_results
2471 WHERE
2472 pk_patient = %%(pat)s
2473 ORDER BY clin_when DESC
2474 LIMIT %s""" % no_of_results
2475 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
2476 return [ cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': r}) for r in rows ]
2477
2478 #------------------------------------------------------------
2480
2481 if None not in [test_type, loinc]:
2482 raise ValueError('either <test_type> or <loinc> must be None')
2483
2484 args = {
2485 'pat': patient,
2486 'ttyp': test_type,
2487 'loinc': loinc
2488 }
2489
2490 where_parts = ['pk_patient = %(pat)s']
2491 if test_type is not None:
2492 where_parts.append('pk_test_type = %(ttyp)s') # consider: pk_meta_test_type = %(pkmtt)s / self._payload[self._idx['pk_meta_test_type']]
2493 elif loinc is not None:
2494 where_parts.append('((loinc_tt IN %(loinc)s) OR (loinc_meta IN %(loinc)s))')
2495 args['loinc'] = tuple(loinc)
2496
2497 cmd = """
2498 SELECT * FROM clin.v_test_results
2499 WHERE
2500 %s
2501 ORDER BY clin_when
2502 LIMIT 1""" % ' AND '.join(where_parts)
2503 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
2504 if len(rows) == 0:
2505 return None
2506
2507 return cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': rows[0]})
2508
2509 #------------------------------------------------------------
2511 try:
2512 pk = int(result)
2513 except (TypeError, AttributeError):
2514 pk = result['pk_test_result']
2515
2516 cmd = 'DELETE FROM clin.test_result WHERE pk = %(pk)s'
2517 gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': {'pk': pk}}])
2518
2519 #------------------------------------------------------------
2520 -def create_test_result(encounter=None, episode=None, type=None, intended_reviewer=None, val_num=None, val_alpha=None, unit=None, link_obj=None):
2521
2522 cmd1 = """
2523 INSERT INTO clin.test_result (
2524 fk_encounter,
2525 fk_episode,
2526 fk_type,
2527 fk_intended_reviewer,
2528 val_num,
2529 val_alpha,
2530 val_unit
2531 ) VALUES (
2532 %(enc)s,
2533 %(epi)s,
2534 %(type)s,
2535 %(rev)s,
2536 %(v_num)s,
2537 %(v_alpha)s,
2538 %(unit)s
2539 )
2540 """
2541 cmd2 = "SELECT * from clin.v_test_results WHERE pk_test_result = currval(pg_get_serial_sequence('clin.test_result', 'pk'))"
2542 args = {
2543 'enc': encounter,
2544 'epi': episode,
2545 'type': type,
2546 'rev': intended_reviewer,
2547 'v_num': val_num,
2548 'v_alpha': val_alpha,
2549 'unit': unit
2550 }
2551 rows, idx = gmPG2.run_rw_queries (
2552 link_obj = link_obj,
2553 queries = [
2554 {'cmd': cmd1, 'args': args},
2555 {'cmd': cmd2}
2556 ],
2557 return_data = True,
2558 get_col_idx = True
2559 )
2560 tr = cTestResult(row = {
2561 'pk_field': 'pk_test_result',
2562 'idx': idx,
2563 'data': rows[0]
2564 })
2565 return tr
2566
2567 #------------------------------------------------------------
2569
2570 _log.debug('formatting test results into [%s]', output_format)
2571
2572 if output_format == 'latex':
2573 return __format_test_results_latex(results = results)
2574
2575 msg = _('unknown test results output format [%s]') % output_format
2576 _log.error(msg)
2577 return msg
2578
2579 #------------------------------------------------------------
2581
2582 if len(results) == 0:
2583 return '\\begin{minipage}{%s} \\end{minipage}' % width
2584
2585 lines = []
2586 for t in results:
2587
2588 tmp = ''
2589
2590 if show_time:
2591 tmp += '{\\tiny (%s)} ' % t['clin_when'].strftime('%H:%M')
2592
2593 tmp += '%.8s' % t['unified_val']
2594
2595 lines.append(tmp)
2596 tmp = ''
2597
2598 if show_range:
2599 has_range = (
2600 t['unified_target_range'] is not None
2601 or
2602 t['unified_target_min'] is not None
2603 or
2604 t['unified_target_max'] is not None
2605 )
2606 if has_range:
2607 if t['unified_target_range'] is not None:
2608 tmp += '{\\tiny %s}' % t['unified_target_range']
2609 else:
2610 tmp += '{\\tiny %s}' % (
2611 gmTools.coalesce(t['unified_target_min'], '- ', '%s - '),
2612 gmTools.coalesce(t['unified_target_max'], '', '%s')
2613 )
2614 lines.append(tmp)
2615
2616 return '\\begin{minipage}{%s} \\begin{flushright} %s \\end{flushright} \\end{minipage}' % (width, ' \\\\ '.join(lines))
2617
2618 #------------------------------------------------------------
2620
2621 if len(results) == 0:
2622 return ''
2623
2624 lines = []
2625 for t in results:
2626
2627 tmp = ''
2628
2629 if show_time:
2630 tmp += '\\tiny %s ' % t['clin_when'].strftime('%H:%M')
2631
2632 tmp += '\\normalsize %.8s' % t['unified_val']
2633
2634 lines.append(tmp)
2635 tmp = '\\tiny %s' % gmTools.coalesce(t['val_unit'], '', '%s ')
2636
2637 if not show_range:
2638 lines.append(tmp)
2639 continue
2640
2641 has_range = (
2642 t['unified_target_range'] is not None
2643 or
2644 t['unified_target_min'] is not None
2645 or
2646 t['unified_target_max'] is not None
2647 )
2648
2649 if not has_range:
2650 lines.append(tmp)
2651 continue
2652
2653 if t['unified_target_range'] is not None:
2654 tmp += '[%s]' % t['unified_target_range']
2655 else:
2656 tmp += '[%s%s]' % (
2657 gmTools.coalesce(t['unified_target_min'], '--', '%s--'),
2658 gmTools.coalesce(t['unified_target_max'], '', '%s')
2659 )
2660 lines.append(tmp)
2661
2662 return ' \\\\ '.join(lines)
2663
2664 #------------------------------------------------------------
2666
2667 if len(results) == 0:
2668 return '\\noindent %s' % _('No test results to format.')
2669
2670 # discover the columns and rows
2671 dates = {}
2672 tests = {}
2673 grid = {}
2674 for result in results:
2675 # row_label = u'%s \\ \\tiny (%s)}' % (result['unified_abbrev'], result['unified_name'])
2676 row_label = result['unified_abbrev']
2677 tests[row_label] = None
2678 col_label = '{\\scriptsize %s}' % result['clin_when'].strftime('%Y-%m-%d')
2679 dates[col_label] = None
2680 try:
2681 grid[row_label]
2682 except KeyError:
2683 grid[row_label] = {}
2684 try:
2685 grid[row_label][col_label].append(result)
2686 except KeyError:
2687 grid[row_label][col_label] = [result]
2688
2689 col_labels = sorted(dates.keys(), reverse = True)
2690 del dates
2691 row_labels = sorted(tests.keys())
2692 del tests
2693
2694 col_def = len(col_labels) * '>{\\raggedleft}p{1.7cm}|'
2695
2696 # format them
2697 tex = """\\noindent %s
2698
2699 \\noindent \\begin{tabular}{|l|%s}
2700 \\hline
2701 & %s \\tabularnewline
2702 \\hline
2703
2704 %%s \\tabularnewline
2705
2706 \\hline
2707
2708 \\end{tabular}""" % (
2709 _('Test results'),
2710 col_def,
2711 ' & '.join(col_labels)
2712 )
2713
2714 rows = []
2715
2716 # loop over rows
2717 for rl in row_labels:
2718 cells = [rl]
2719 # loop over cols per row
2720 for cl in col_labels:
2721 try:
2722 # get tests for this (row/col) position
2723 tests = grid[rl][cl]
2724 except KeyError:
2725 # none there, so insert empty cell
2726 cells.append(' ')
2727 continue
2728
2729 cells.append (
2730 __tests2latex_cell (
2731 results = tests,
2732 show_time = (len(tests) > 1),
2733 show_range = True
2734 )
2735 )
2736
2737 rows.append(' & '.join(cells))
2738
2739 return tex % ' \\tabularnewline\n \\hline\n'.join(rows)
2740
2741 #============================================================
2743
2744 sandbox_dir = os.path.join(gmTools.gmPaths().tmp_dir, 'gplot')
2745 if not gmTools.mkdir(sandbox_dir):
2746 sandbox_dir = gmTools.mk_sandbox_dir(prefix = 'gm2gpl-')
2747 _log.debug('sandbox directory: [%s]', sandbox_dir)
2748 if filename is None:
2749 filename = gmTools.get_unique_filename(prefix = 'gm2gpl-', suffix = '.dat', tmp_dir = sandbox_dir)
2750
2751 # sort results into series by test type
2752 series = {}
2753 for r in results:
2754 try:
2755 series[r['unified_name']].append(r)
2756 except KeyError:
2757 series[r['unified_name']] = [r]
2758
2759 conf_name = '%s.conf' % filename
2760 gplot_conf = io.open(conf_name, mode = 'wt', encoding = 'utf8')
2761 gplot_conf.write('# settings for stacked multiplot layouts:\n')
2762 sub_title = _('plotted %s (GNUmed v%s)') % (
2763 gmDateTime.pydt_now_here().strftime('%Y %b %d %H:%M'),
2764 _cfg.get(option = 'client_version')
2765 )
2766 if patient is None:
2767 plot_title = sub_title
2768 else:
2769 plot_title = '%s - %s\\n%s' % (
2770 patient.get_description_gender(with_nickname = False).strip(),
2771 patient.get_formatted_dob(format = '%Y %b %d', none_string = _('unknown DOB'), honor_estimation = True),
2772 sub_title
2773 )
2774 gplot_conf.write('multiplot_title = "%s"\n' % plot_title)
2775 gplot_conf.write('multiplot_no_of_tests = %s # number of index blocks (resp. test types)\n' % len(series))
2776 gplot_conf.write('array multiplot_y_labels[multiplot_no_of_tests] # list for ylabels suitable for stacked multiplots\n')
2777 gplot_conf.write('\n')
2778 gplot_conf.write('# settings for individual plots, stacked or not:\n')
2779
2780 gplot_data = io.open(filename, mode = 'wt', encoding = 'utf8')
2781 gplot_data.write('# -------------------------------------------------------------\n')
2782 gplot_data.write('# GNUmed test results export for Gnuplot plotting\n')
2783 gplot_data.write('# -------------------------------------------------------------\n')
2784 gplot_data.write('# first line of each index: test type abbreviation & name,\n')
2785 gplot_data.write('# can be used as title for plots: set key ... autotitle columnheader\n')
2786 gplot_data.write('#\n')
2787 gplot_data.write('# Columns in each index:\n')
2788 gplot_data.write('# 1 - clin_when at full precision\n')
2789 gplot_data.write('# set timefmt "%Y-%m-%d_%H:%M"\n')
2790 gplot_data.write('# timecolumn(1, "%Y-%m-%d_%H:%M")\n')
2791 gplot_data.write('# 2 - value\n')
2792 gplot_data.write('# 3 - unit\n')
2793 gplot_data.write('# 4 - unified (target or normal) range: lower bound\n')
2794 gplot_data.write('# 5 - unified (target or normal) range: upper bound\n')
2795 gplot_data.write('# 6 - normal range: lower bound\n')
2796 gplot_data.write('# 7 - normal range: upper bound\n')
2797 gplot_data.write('# 8 - target range: lower bound\n')
2798 gplot_data.write('# 9 - target range: upper bound\n')
2799 gplot_data.write('# 10 - clin_when formatted into string (say, as x-axis tic label)\n')
2800 gplot_data.write('#\n')
2801 gplot_data.write('# index rows are NOT sorted by clin_when, so plotting\n')
2802 gplot_data.write('# with lined styles will make the lines go all over\n')
2803 gplot_data.write('# -------------------------------------------------------------\n')
2804 gplot_data.write('#\n')
2805 gplot_data.write('# the file <%s.conf>\n' % filename)
2806 gplot_data.write('# will contain various gnuplot settings specific to this plot,\n')
2807 gplot_data.write('# such as <ylabel>, <y2label>, <title>,\n')
2808 gplot_data.write('# there will also be settings suitable for stacked multiplots\n')
2809 gplot_data.write('# -------------------------------------------------------------\n')
2810
2811 series_keys = list(series.keys())
2812 for test_type_idx in range(len(series_keys)):
2813 test_type = series_keys[test_type_idx]
2814 if len(series[test_type]) == 0:
2815 continue
2816 result = series[test_type][0]
2817 if test_type_idx == 0:
2818 gplot_conf.write('set title "%s" enhanced\n' % plot_title)
2819 gplot_conf.write('\n')
2820 gplot_conf.write('set ylabel "%s"\n' % result['unified_name'])
2821 elif test_type_idx == 1:
2822 gplot_conf.write('set y2label "%s"\n' % result['unified_name'])
2823 gplot_conf.write('multiplot_y_labels[%s] = "%s (%s)"\n' % (test_type_idx + 1, result['unified_name'], result['unified_abbrev']))
2824 title = '%s (%s)' % (
2825 result['unified_abbrev'],
2826 result['unified_name']
2827 )
2828 gplot_data.write('\n\n"%s" "%s"\n' % (title, title))
2829 prev_date = None
2830 prev_year = None
2831 for result in series[test_type]:
2832 curr_date = gmDateTime.pydt_strftime(result['clin_when'], '%Y-%m-%d', 'utf8', gmDateTime.acc_days)
2833 if curr_date == prev_date:
2834 gplot_data.write('\n# %s\n' % _('blank line inserted to allow for discontinued line drawing of same-day values'))
2835 if show_year:
2836 if result['clin_when'].year == prev_year:
2837 when_template = '%b %d %H:%M'
2838 else:
2839 when_template = '%b %d %H:%M (%Y)'
2840 prev_year = result['clin_when'].year
2841 else:
2842 when_template = '%b %d'
2843 val = result['val_num']
2844 if val is None:
2845 val = result.estimate_numeric_value_from_alpha
2846 if val is None:
2847 continue # skip distinctly non-numericable values
2848 gplot_data.write ('%s %s "%s" %s %s %s %s %s %s "%s"\n' % (
2849 #result['clin_when'].strftime('%Y-%m-%d_%H:%M'),
2850 gmDateTime.pydt_strftime(result['clin_when'], '%Y-%m-%d_%H:%M', 'utf8', gmDateTime.acc_minutes),
2851 val,
2852 gmTools.coalesce(result['val_unit'], '"<?>"'),
2853 gmTools.coalesce(result['unified_target_min'], '"<?>"'),
2854 gmTools.coalesce(result['unified_target_max'], '"<?>"'),
2855 gmTools.coalesce(result['val_normal_min'], '"<?>"'),
2856 gmTools.coalesce(result['val_normal_max'], '"<?>"'),
2857 gmTools.coalesce(result['val_target_min'], '"<?>"'),
2858 gmTools.coalesce(result['val_target_max'], '"<?>"'),
2859 gmDateTime.pydt_strftime (
2860 result['clin_when'],
2861 format = when_template,
2862 accuracy = gmDateTime.acc_minutes
2863 )
2864 ))
2865 prev_date = curr_date
2866 gplot_data.close()
2867 gplot_conf.close()
2868
2869 return filename
2870
2871 #============================================================
2873 """Represents one lab result."""
2874
2875 _cmd_fetch_payload = """
2876 select *, xmin_test_result from v_results4lab_req
2877 where pk_result=%s"""
2878 _cmds_lock_rows_for_update = [
2879 """select 1 from test_result where pk=%(pk_result)s and xmin=%(xmin_test_result)s for update"""
2880 ]
2881 _cmds_store_payload = [
2882 """update test_result set
2883 clin_when = %(val_when)s,
2884 narrative = %(progress_note_result)s,
2885 fk_type = %(pk_test_type)s,
2886 val_num = %(val_num)s::numeric,
2887 val_alpha = %(val_alpha)s,
2888 val_unit = %(val_unit)s,
2889 val_normal_min = %(val_normal_min)s,
2890 val_normal_max = %(val_normal_max)s,
2891 val_normal_range = %(val_normal_range)s,
2892 val_target_min = %(val_target_min)s,
2893 val_target_max = %(val_target_max)s,
2894 val_target_range = %(val_target_range)s,
2895 abnormality_indicator = %(abnormal)s,
2896 norm_ref_group = %(ref_group)s,
2897 note_provider = %(note_provider)s,
2898 material = %(material)s,
2899 material_detail = %(material_detail)s
2900 where pk = %(pk_result)s""",
2901 """select xmin_test_result from v_results4lab_req where pk_result=%(pk_result)s"""
2902 ]
2903
2904 _updatable_fields = [
2905 'val_when',
2906 'progress_note_result',
2907 'val_num',
2908 'val_alpha',
2909 'val_unit',
2910 'val_normal_min',
2911 'val_normal_max',
2912 'val_normal_range',
2913 'val_target_min',
2914 'val_target_max',
2915 'val_target_range',
2916 'abnormal',
2917 'ref_group',
2918 'note_provider',
2919 'material',
2920 'material_detail'
2921 ]
2922 #--------------------------------------------------------
2924 """Instantiate.
2925
2926 aPK_obj as dict:
2927 - patient_id
2928 - when_field (see view definition)
2929 - when
2930 - test_type
2931 - val_num
2932 - val_alpha
2933 - unit
2934 """
2935 # instantiate from row data ?
2936 if aPK_obj is None:
2937 gmBusinessDBObject.cBusinessDBObject.__init__(self, row=row)
2938 return
2939 pk = aPK_obj
2940 # find PK from row data ?
2941 if type(aPK_obj) == dict:
2942 # sanity checks
2943 if None in [aPK_obj['patient_id'], aPK_obj['when'], aPK_obj['when_field'], aPK_obj['test_type'], aPK_obj['unit']]:
2944 raise gmExceptions.ConstructorError('parameter error: %s' % aPK_obj)
2945 if (aPK_obj['val_num'] is None) and (aPK_obj['val_alpha'] is None):
2946 raise gmExceptions.ConstructorError('parameter error: val_num and val_alpha cannot both be None')
2947 # get PK
2948 where_snippets = [
2949 'pk_patient=%(patient_id)s',
2950 'pk_test_type=%(test_type)s',
2951 '%s=%%(when)s' % aPK_obj['when_field'],
2952 'val_unit=%(unit)s'
2953 ]
2954 if aPK_obj['val_num'] is not None:
2955 where_snippets.append('val_num=%(val_num)s::numeric')
2956 if aPK_obj['val_alpha'] is not None:
2957 where_snippets.append('val_alpha=%(val_alpha)s')
2958
2959 where_clause = ' and '.join(where_snippets)
2960 cmd = "select pk_result from v_results4lab_req where %s" % where_clause
2961 data = gmPG.run_ro_query('historica', cmd, None, aPK_obj)
2962 if data is None:
2963 raise gmExceptions.ConstructorError('error getting lab result for: %s' % aPK_obj)
2964 if len(data) == 0:
2965 raise gmExceptions.NoSuchClinItemError('no lab result for: %s' % aPK_obj)
2966 pk = data[0][0]
2967 # instantiate class
2968 gmBusinessDBObject.cBusinessDBObject.__init__(self, aPK_obj=pk)
2969 #--------------------------------------------------------
2971 cmd = """
2972 select
2973 %s,
2974 vbp.title,
2975 vbp.firstnames,
2976 vbp.lastnames,
2977 vbp.dob
2978 from v_active_persons vbp
2979 where vbp.pk_identity = %%s""" % self._payload[self._idx['pk_patient']]
2980 pat = gmPG.run_ro_query('historica', cmd, None, self._payload[self._idx['pk_patient']])
2981 return pat[0]
2982
2983 #============================================================
2985 """Represents one lab request."""
2986
2987 _cmd_fetch_payload = """
2988 select *, xmin_lab_request from v_lab_requests
2989 where pk_request=%s"""
2990 _cmds_lock_rows_for_update = [
2991 """select 1 from lab_request where pk=%(pk_request)s and xmin=%(xmin_lab_request)s for update"""
2992 ]
2993 _cmds_store_payload = [
2994 """update lab_request set
2995 request_id=%(request_id)s,
2996 lab_request_id=%(lab_request_id)s,
2997 clin_when=%(sampled_when)s,
2998 lab_rxd_when=%(lab_rxd_when)s,
2999 results_reported_when=%(results_reported_when)s,
3000 request_status=%(request_status)s,
3001 is_pending=%(is_pending)s::bool,
3002 narrative=%(progress_note)s
3003 where pk=%(pk_request)s""",
3004 """select xmin_lab_request from v_lab_requests where pk_request=%(pk_request)s"""
3005 ]
3006 _updatable_fields = [
3007 'request_id',
3008 'lab_request_id',
3009 'sampled_when',
3010 'lab_rxd_when',
3011 'results_reported_when',
3012 'request_status',
3013 'is_pending',
3014 'progress_note'
3015 ]
3016 #--------------------------------------------------------
3018 """Instantiate lab request.
3019
3020 The aPK_obj can be either a dict with the keys "req_id"
3021 and "lab" or a simple primary key.
3022 """
3023 # instantiate from row data ?
3024 if aPK_obj is None:
3025 gmBusinessDBObject.cBusinessDBObject.__init__(self, row=row)
3026 return
3027 pk = aPK_obj
3028 # instantiate from "req_id" and "lab" ?
3029 if type(aPK_obj) == dict:
3030 # sanity check
3031 try:
3032 aPK_obj['req_id']
3033 aPK_obj['lab']
3034 except Exception:
3035 _log.exception('[%s:??]: faulty <aPK_obj> structure: [%s]' % (self.__class__.__name__, aPK_obj), sys.exc_info())
3036 raise gmExceptions.ConstructorError('[%s:??]: cannot derive PK from [%s]' % (self.__class__.__name__, aPK_obj))
3037 # generate query
3038 where_snippets = []
3039 vals = {}
3040 where_snippets.append('request_id=%(req_id)s')
3041 if type(aPK_obj['lab']) == int:
3042 where_snippets.append('pk_test_org=%(lab)s')
3043 else:
3044 where_snippets.append('lab_name=%(lab)s')
3045 where_clause = ' and '.join(where_snippets)
3046 cmd = "select pk_request from v_lab_requests where %s" % where_clause
3047 # get pk
3048 data = gmPG.run_ro_query('historica', cmd, None, aPK_obj)
3049 if data is None:
3050 raise gmExceptions.ConstructorError('[%s:??]: error getting lab request for [%s]' % (self.__class__.__name__, aPK_obj))
3051 if len(data) == 0:
3052 raise gmExceptions.NoSuchClinItemError('[%s:??]: no lab request for [%s]' % (self.__class__.__name__, aPK_obj))
3053 pk = data[0][0]
3054 # instantiate class
3055 gmBusinessDBObject.cBusinessDBObject.__init__(self, aPK_obj=pk)
3056 #--------------------------------------------------------
3058 cmd = """
3059 select vpi.pk_patient, vbp.title, vbp.firstnames, vbp.lastnames, vbp.dob
3060 from v_pat_items vpi, v_active_persons vbp
3061 where
3062 vpi.pk_item=%s
3063 and
3064 vbp.pk_identity=vpi.pk_patient"""
3065 pat = gmPG.run_ro_query('historica', cmd, None, self._payload[self._idx['pk_item']])
3066 if pat is None:
3067 _log.error('cannot get patient for lab request [%s]' % self._payload[self._idx['pk_item']])
3068 return None
3069 if len(pat) == 0:
3070 _log.error('no patient associated with lab request [%s]' % self._payload[self._idx['pk_item']])
3071 return None
3072 return pat[0]
3073
3074 #============================================================
3075 # convenience functions
3076 #------------------------------------------------------------
3077 -def create_lab_request(lab=None, req_id=None, pat_id=None, encounter_id=None, episode_id=None):
3078 """Create or get lab request.
3079
3080 returns tuple (status, value):
3081 (True, lab request instance)
3082 (False, error message)
3083 (None, housekeeping_todo primary key)
3084 """
3085 req = None
3086 aPK_obj = {
3087 'lab': lab,
3088 'req_id': req_id
3089 }
3090 try:
3091 req = cLabRequest (aPK_obj)
3092 except gmExceptions.NoSuchClinItemError as msg:
3093 _log.info('%s: will try to create lab request' % str(msg))
3094 except gmExceptions.ConstructorError as msg:
3095 _log.exception(str(msg), sys.exc_info(), verbose=0)
3096 return (False, msg)
3097 # found
3098 if req is not None:
3099 db_pat = req.get_patient()
3100 if db_pat is None:
3101 _log.error('cannot cross-check patient on lab request')
3102 return (None, '')
3103 # yes but ambigous
3104 if pat_id != db_pat[0]:
3105 _log.error('lab request found for [%s:%s] but patient mismatch: expected [%s], in DB [%s]' % (lab, req_id, pat_id, db_pat))
3106 me = '$RCSfile: gmPathLab.py,v $ $Revision: 1.81 $'
3107 to = 'user'
3108 prob = _('The lab request already exists but belongs to a different patient.')
3109 sol = _('Verify which patient this lab request really belongs to.')
3110 ctxt = _('lab [%s], request ID [%s], expected link with patient [%s], currently linked to patient [%s]') % (lab, req_id, pat_id, db_pat)
3111 cat = 'lab'
3112 status, data = gmPG.add_housekeeping_todo(me, to, prob, sol, ctxt, cat)
3113 return (None, data)
3114 return (True, req)
3115 # not found
3116 queries = []
3117 if type(lab) is int:
3118 cmd = "insert into lab_request (fk_encounter, fk_episode, fk_test_org, request_id) values (%s, %s, %s, %s)"
3119 else:
3120 cmd = "insert into lab_request (fk_encounter, fk_episode, fk_test_org, request_id) values (%s, %s, (select pk from test_org where internal_OBSOLETE_name=%s), %s)"
3121 queries.append((cmd, [encounter_id, episode_id, str(lab), req_id]))
3122 cmd = "select currval('lab_request_pk_seq')"
3123 queries.append((cmd, []))
3124 # insert new
3125 result, err = gmPG.run_commit('historica', queries, True)
3126 if result is None:
3127 return (False, err)
3128 try:
3129 req = cLabRequest(aPK_obj=result[0][0])
3130 except gmExceptions.ConstructorError as msg:
3131 _log.exception(str(msg), sys.exc_info(), verbose=0)
3132 return (False, msg)
3133 return (True, req)
3134 #------------------------------------------------------------
3135 -def create_lab_result(patient_id=None, when_field=None, when=None, test_type=None, val_num=None, val_alpha=None, unit=None, encounter_id=None, request=None):
3136 tres = None
3137 data = {
3138 'patient_id': patient_id,
3139 'when_field': when_field,
3140 'when': when,
3141 'test_type': test_type,
3142 'val_num': val_num,
3143 'val_alpha': val_alpha,
3144 'unit': unit
3145 }
3146 try:
3147 tres = cLabResult(aPK_obj=data)
3148 # exists already, so fail
3149 _log.error('will not overwrite existing test result')
3150 _log.debug(str(tres))
3151 return (None, tres)
3152 except gmExceptions.NoSuchClinItemError:
3153 _log.debug('test result not found - as expected, will create it')
3154 except gmExceptions.ConstructorError as msg:
3155 _log.exception(str(msg), sys.exc_info(), verbose=0)
3156 return (False, msg)
3157 if request is None:
3158 return (False, _('need lab request when inserting lab result'))
3159 # not found
3160 if encounter_id is None:
3161 encounter_id = request['pk_encounter']
3162 queries = []
3163 cmd = "insert into test_result (fk_encounter, fk_episode, fk_type, val_num, val_alpha, val_unit) values (%s, %s, %s, %s, %s, %s)"
3164 queries.append((cmd, [encounter_id, request['pk_episode'], test_type, val_num, val_alpha, unit]))
3165 cmd = "insert into lnk_result2lab_req (fk_result, fk_request) values ((select currval('test_result_pk_seq')), %s)"
3166 queries.append((cmd, [request['pk_request']]))
3167 cmd = "select currval('test_result_pk_seq')"
3168 queries.append((cmd, []))
3169 # insert new
3170 result, err = gmPG.run_commit('historica', queries, True)
3171 if result is None:
3172 return (False, err)
3173 try:
3174 tres = cLabResult(aPK_obj=result[0][0])
3175 except gmExceptions.ConstructorError as msg:
3176 _log.exception(str(msg), sys.exc_info(), verbose=0)
3177 return (False, msg)
3178 return (True, tres)
3179 #------------------------------------------------------------
3181 # sanity check
3182 if limit < 1:
3183 limit = 1
3184 # retrieve one more row than needed so we know there's more available ;-)
3185 lim = limit + 1
3186 cmd = """
3187 select pk_result
3188 from v_results4lab_req
3189 where reviewed is false
3190 order by pk_patient
3191 limit %s""" % lim
3192 rows = gmPG.run_ro_query('historica', cmd)
3193 if rows is None:
3194 _log.error('error retrieving unreviewed lab results')
3195 return (None, _('error retrieving unreviewed lab results'))
3196 if len(rows) == 0:
3197 return (False, [])
3198 # more than LIMIT rows ?
3199 if len(rows) == lim:
3200 more_avail = True
3201 # but deliver only LIMIT rows so that our assumption holds true...
3202 del rows[limit]
3203 else:
3204 more_avail = False
3205 results = []
3206 for row in rows:
3207 try:
3208 results.append(cLabResult(aPK_obj=row[0]))
3209 except gmExceptions.ConstructorError:
3210 _log.exception('skipping unreviewed lab result [%s]' % row[0], sys.exc_info(), verbose=0)
3211 return (more_avail, results)
3212
3213 #------------------------------------------------------------
3215 lim = limit + 1
3216 cmd = "select pk from lab_request where is_pending is true limit %s" % lim
3217 rows = gmPG.run_ro_query('historica', cmd)
3218 if rows is None:
3219 _log.error('error retrieving pending lab requests')
3220 return (None, None)
3221 if len(rows) == 0:
3222 return (False, [])
3223 results = []
3224 # more than LIMIT rows ?
3225 if len(rows) == lim:
3226 too_many = True
3227 # but deliver only LIMIT rows so that our assumption holds true...
3228 del rows[limit]
3229 else:
3230 too_many = False
3231 requests = []
3232 for row in rows:
3233 try:
3234 requests.append(cLabRequest(aPK_obj=row[0]))
3235 except gmExceptions.ConstructorError:
3236 _log.exception('skipping pending lab request [%s]' % row[0], sys.exc_info(), verbose=0)
3237 return (too_many, requests)
3238
3239 #------------------------------------------------------------
3241 """Get logically next request ID for given lab.
3242
3243 - incrementor_func:
3244 - if not supplied the next ID is guessed
3245 - if supplied it is applied to the most recently used ID
3246 """
3247 if type(lab) == int:
3248 lab_snippet = 'vlr.fk_test_org=%s'
3249 else:
3250 lab_snippet = 'vlr.lab_name=%s'
3251 lab = str(lab)
3252 cmd = """
3253 select request_id
3254 from lab_request lr0
3255 where lr0.clin_when = (
3256 select max(vlr.sampled_when)
3257 from v_lab_requests vlr
3258 where %s
3259 )""" % lab_snippet
3260 rows = gmPG.run_ro_query('historica', cmd, None, lab)
3261 if rows is None:
3262 _log.warning('error getting most recently used request ID for lab [%s]' % lab)
3263 return ''
3264 if len(rows) == 0:
3265 return ''
3266 most_recent = rows[0][0]
3267 # apply supplied incrementor
3268 if incrementor_func is not None:
3269 try:
3270 next = incrementor_func(most_recent)
3271 except TypeError:
3272 _log.error('cannot call incrementor function [%s]' % str(incrementor_func))
3273 return most_recent
3274 return next
3275 # try to be smart ourselves
3276 for pos in range(len(most_recent)):
3277 header = most_recent[:pos]
3278 trailer = most_recent[pos:]
3279 try:
3280 return '%s%s' % (header, str(int(trailer) + 1))
3281 except ValueError:
3282 header = most_recent[:-1]
3283 trailer = most_recent[-1:]
3284 return '%s%s' % (header, chr(ord(trailer) + 1))
3285
3286 #============================================================
3288 """Calculate BMI.
3289
3290 mass: kg
3291 height: cm
3292 age: not yet used
3293
3294 returns:
3295 (True/False, data)
3296 True: data = (bmi, lower_normal, upper_normal)
3297 False: data = error message
3298 """
3299 converted, mass = gmTools.input2decimal(mass)
3300 if not converted:
3301 return False, 'mass: cannot convert <%s> to Decimal' % mass
3302
3303 converted, height = gmTools.input2decimal(height)
3304 if not converted:
3305 return False, 'height: cannot convert <%s> to Decimal' % height
3306
3307 approx_surface = (height / decimal.Decimal(100))**2
3308 bmi = mass / approx_surface
3309
3310 print(mass, height, '->', approx_surface, '->', bmi)
3311
3312 lower_normal_mass = 20.0 * approx_surface
3313 upper_normal_mass = 25.0 * approx_surface
3314
3315 return True, (bmi, lower_normal_mass, upper_normal_mass)
3316
3317 #============================================================
3318 # main - unit testing
3319 #------------------------------------------------------------
3320 if __name__ == '__main__':
3321
3322 if len(sys.argv) < 2:
3323 sys.exit()
3324
3325 if sys.argv[1] != 'test':
3326 sys.exit()
3327
3328 import time
3329
3330 gmI18N.activate_locale()
3331 gmI18N.install_domain()
3332
3333 #------------------------------------------
3335 tr = create_test_result (
3336 encounter = 1,
3337 episode = 1,
3338 type = 1,
3339 intended_reviewer = 1,
3340 val_num = '12',
3341 val_alpha=None,
3342 unit = 'mg/dl'
3343 )
3344 print(tr)
3345 return tr
3346 #------------------------------------------
3350 #------------------------------------------
3352 r = cTestResult(aPK_obj=6)
3353 #print r
3354 #print r.reference_ranges
3355 #print r.formatted_range
3356 #print r.temporally_closest_normal_range
3357 print(r.estimate_numeric_value_from_alpha)
3358 #------------------------------------------
3360 print("test_result()")
3361 # lab_result = cLabResult(aPK_obj=4)
3362 data = {
3363 'patient_id': 12,
3364 'when_field': 'val_when',
3365 'when': '2000-09-17 18:23:00+02',
3366 'test_type': 9,
3367 'val_num': 17.3,
3368 'val_alpha': None,
3369 'unit': 'mg/l'
3370 }
3371 lab_result = cLabResult(aPK_obj=data)
3372 print(lab_result)
3373 fields = lab_result.get_fields()
3374 for field in fields:
3375 print(field, ':', lab_result[field])
3376 print("updatable:", lab_result.get_updatable_fields())
3377 print(time.time())
3378 print(lab_result.get_patient())
3379 print(time.time())
3380 #------------------------------------------
3382 print("test_request()")
3383 try:
3384 # lab_req = cLabRequest(aPK_obj=1)
3385 # lab_req = cLabRequest(req_id='EML#SC937-0176-CEC#11', lab=2)
3386 data = {
3387 'req_id': 'EML#SC937-0176-CEC#11',
3388 'lab': 'Enterprise Main Lab'
3389 }
3390 lab_req = cLabRequest(aPK_obj=data)
3391 except gmExceptions.ConstructorError as msg:
3392 print("no such lab request:", msg)
3393 return
3394 print(lab_req)
3395 fields = lab_req.get_fields()
3396 for field in fields:
3397 print(field, ':', lab_req[field])
3398 print("updatable:", lab_req.get_updatable_fields())
3399 print(time.time())
3400 print(lab_req.get_patient())
3401 print(time.time())
3402 #--------------------------------------------------------
3407 #--------------------------------------------------------
3412 #--------------------------------------------------------
3414 print(create_measurement_type (
3415 lab = None,
3416 abbrev = 'tBZ2',
3417 unit = 'mg%',
3418 name = 'BZ (test 2)'
3419 ))
3420 #--------------------------------------------------------
3425 #--------------------------------------------------------
3430 #--------------------------------------------------------
3432 results = [
3433 cTestResult(aPK_obj=1),
3434 cTestResult(aPK_obj=2),
3435 cTestResult(aPK_obj=3)
3436 # cTestResult(aPK_obj=4)
3437 ]
3438 print(format_test_results(results = results))
3439 #--------------------------------------------------------
3441 done, data = calculate_bmi(mass = sys.argv[2], height = sys.argv[3])
3442 bmi, low, high = data
3443 print("BMI:", bmi)
3444 print("low:", low, "kg")
3445 print("hi :", high, "kg")
3446
3447 #--------------------------------------------------------
3453
3454 #--------------------------------------------------------
3456 tp = cTestPanel(aPK_obj = 1)
3457 #print tp.included_loincs
3458 #tp = cTestPanel(aPK_obj = 3)
3459 print(tp.format())
3460 #most_recent = tp.get_most_recent_results(pk_patient = 12, group_by_meta_type = False)
3461 #most_recent = tp.get_most_recent_results(pk_patient = 138, group_by_meta_type = False)
3462 #print len(most_recent)
3463 most_recent = tp.get_most_recent_results(pk_patient = 12, group_by_meta_type = True, include_missing = True)
3464 #most_recent = tp.get_most_recent_results(pk_patient = 138, group_by_meta_type = True)
3465 print('found:', len(most_recent))
3466
3467 for t in most_recent:
3468 print('--------------')
3469 if t['pk_meta_test_type'] is None:
3470 print("standalone")
3471 else:
3472 print("meta")
3473 print(t.format())
3474
3475 #--------------------------------------------------------
3477 most_recent = get_most_recent_results_in_loinc_group (
3478 #loincs = [u'pseudo LOINC [C-reactive protein (EML)::9] (v21->v22 test panel conversion)'],
3479 #loincs = ['8867-4'],
3480 loincs = ['2160-0', '14682-9', '40264-4', '40248-7'],
3481 max_no_of_results = 6,
3482 patient = 201,
3483 consider_indirect_matches = False
3484 )
3485 for t in most_recent:
3486 print(t['loinc_tt'], t['loinc_meta'], t['unified_loinc'])
3487 if t['pk_meta_test_type'] is None:
3488 print("---- standalone ----")
3489 else:
3490 print("---- meta ----")
3491 print(t.format())
3492 input('next')
3493
3494 return
3495
3496 most_recent = get_most_recent_results_in_loinc_group (
3497 #loincs = [u'pseudo LOINC [C-reactive protein (EML)::9] (v21->v22 test panel conversion)'],
3498 #loincs = ['8867-4'],
3499 loincs = ['2160-0', '14682-9', '40264-4', '40248-7'],
3500 max_no_of_results = 2,
3501 patient = 201,
3502 consider_indirect_matches = False
3503 #consider_indirect_matches = True
3504 )
3505 for t in most_recent:
3506 if t['pk_meta_test_type'] is None:
3507 print("---- standalone ----")
3508 else:
3509 print("---- meta ----")
3510 print(t.format())
3511
3512 #--------------------------------------------------------
3513
3514 #test_result()
3515 #test_create_test_result()
3516 #test_delete_test_result()
3517 #test_create_measurement_type()
3518 #test_lab_result()
3519 #test_request()
3520 #test_create_result()
3521 #test_unreviewed()
3522 #test_pending()
3523 #test_meta_test_type()
3524 #test_test_type()
3525 #test_format_test_results()
3526 #test_calculate_bmi()
3527 #test_test_panel()
3528 #test_get_most_recent_results_for_panel()
3529 test_get_most_recent_results_in_loinc_group()
3530
3531 #============================================================
3532
| Home | Trees | Indices | Help |
|
|---|
| Generated by Epydoc 3.0.1 on Sat Feb 29 02:55:27 2020 | http://epydoc.sourceforge.net |