1 """GNUmed measurements related business objects."""
2
3
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
70 -class cTestOrg(gmBusinessDBObject.cBusinessDBObject):
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
92 -def create_test_org(name=None, comment=None, pk_org_unit=None, link_obj=None):
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
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
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
152
153 _SQL_get_test_panels = "SELECT * FROM clin.v_test_panels WHERE %s"
154
155 -class cTestPanel(gmBusinessDBObject.cBusinessDBObject):
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
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
323
325 return self._payload[self._idx['loincs']]
326
328 queries = []
329
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
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
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
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
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
632
633
661
662
675
676
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
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
748
749
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
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
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
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
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
1033
1034
1035
1036
1037
1038 cols = []
1039 val_snippets = []
1040 vals = {}
1041
1042
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
1055 cols.append('abbrev')
1056 val_snippets.append('%(abbrev)s')
1057 vals['abbrev'] = abbrev
1058
1059
1060 if unit is not None:
1061 cols.append('reference_unit')
1062 val_snippets.append('%(unit)s')
1063 vals['unit'] = unit
1064
1065
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
1083 -class cTestResult(gmBusinessDBObject.cBusinessDBObject):
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
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
1183
1184
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
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
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
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
1660 if self._payload[self._idx['is_technically_abnormal']] is False:
1661 return False
1662
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
1672 if self._payload[self._idx['val_num']] is None:
1673 return None
1674
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
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
1691 if self._payload[self._idx['is_technically_abnormal']] is False:
1692 return False
1693
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
1703 if self._payload[self._idx['val_num']] is None:
1704 return None
1705
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
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
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
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
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
1873
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
1931 earlier_results = []
1932
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
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
1946 later_results = []
1947
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
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
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
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
2047
2048
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
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
2092
2093
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
2143
2144
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
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')
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
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')
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
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')
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
2578
2579
2580 -def __tests2latex_minipage(results=None, width='1.5cm', show_time=False, show_range=True):
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
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
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)
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
2848 gplot_data.write ('%s %s "%s" %s %s %s %s %s %s "%s"\n' % (
2849
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
2872 -class cLabResult(gmBusinessDBObject.cBusinessDBObject):
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
2923 - def __init__(self, aPK_obj=None, row=None):
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
2936 if aPK_obj is None:
2937 gmBusinessDBObject.cBusinessDBObject.__init__(self, row=row)
2938 return
2939 pk = aPK_obj
2940
2941 if type(aPK_obj) == dict:
2942
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
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
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
2984 -class cLabRequest(gmBusinessDBObject.cBusinessDBObject):
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
3017 - def __init__(self, aPK_obj=None, row=None):
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
3024 if aPK_obj is None:
3025 gmBusinessDBObject.cBusinessDBObject.__init__(self, row=row)
3026 return
3027 pk = aPK_obj
3028
3029 if type(aPK_obj) == dict:
3030
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
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
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
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
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
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
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
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
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
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
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
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
3182 if limit < 1:
3183 limit = 1
3184
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
3199 if len(rows) == lim:
3200 more_avail = True
3201
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
3225 if len(rows) == lim:
3226 too_many = True
3227
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
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
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
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
3358
3360 print("test_result()")
3361
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
3385
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
3420
3425
3430
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
3458
3459 print(tp.format())
3460
3461
3462
3463 most_recent = tp.get_most_recent_results(pk_patient = 12, group_by_meta_type = True, include_missing = True)
3464
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
3479
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
3498
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
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
3515
3516
3517
3518
3519
3520
3521
3522
3523
3524
3525
3526
3527
3528
3529 test_get_most_recent_results_in_loinc_group()
3530
3531
3532