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
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
960
961
963 args = {}
964 where_parts = []
965 if loincs is not None:
966 if len(loincs) > 0:
967 where_parts.append('loinc IN %(loincs)s')
968 args['loincs'] = tuple(loincs)
969 if len(where_parts) == 0:
970 where_parts.append('TRUE')
971 WHERE_clause = ' AND '.join(where_parts)
972 cmd = (_SQL_get_test_types % WHERE_clause) + gmTools.coalesce(order_by, '', ' ORDER BY %s')
973
974 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
975 if return_pks:
976 return [ r['pk_test_type'] for r in rows ]
977 return [ cMeasurementType(row = {'pk_field': 'pk_test_type', 'data': r, 'idx': idx}) for r in rows ]
978
979
981
982 if (abbrev is None) and (name is None):
983 raise ValueError('must have <abbrev> and/or <name> set')
984
985 where_snippets = []
986
987 if lab is None:
988 where_snippets.append('pk_test_org IS NULL')
989 else:
990 try:
991 int(lab)
992 where_snippets.append('pk_test_org = %(lab)s')
993 except (TypeError, ValueError):
994 where_snippets.append('pk_test_org = (SELECT pk_test_org FROM clin.v_test_orgs WHERE unit = %(lab)s)')
995
996 if abbrev is not None:
997 where_snippets.append('abbrev = %(abbrev)s')
998
999 if name is not None:
1000 where_snippets.append('name = %(name)s')
1001
1002 where_clause = ' and '.join(where_snippets)
1003 cmd = "select * from clin.v_test_types where %s" % where_clause
1004 args = {'lab': lab, 'abbrev': abbrev, 'name': name}
1005
1006 rows, idx = gmPG2.run_ro_queries(link_obj = link_obj, queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
1007
1008 if len(rows) == 0:
1009 return None
1010
1011 tt = cMeasurementType(row = {'pk_field': 'pk_test_type', 'data': rows[0], 'idx': idx})
1012 return tt
1013
1014
1016 cmd = 'delete from clin.test_type where pk = %(pk)s'
1017 args = {'pk': measurement_type}
1018 gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
1019
1020
1022 """Create or get test type."""
1023
1024 ttype = find_measurement_type(lab = lab, abbrev = abbrev, name = name, link_obj = link_obj)
1025
1026 if ttype is not None:
1027 return ttype
1028
1029 _log.debug('creating test type [%s:%s:%s:%s]', lab, abbrev, name, unit)
1030
1031
1032
1033
1034
1035
1036
1037 cols = []
1038 val_snippets = []
1039 vals = {}
1040
1041
1042 if lab is None:
1043 lab = create_test_org(link_obj = link_obj)['pk_test_org']
1044
1045 cols.append('fk_test_org')
1046 try:
1047 vals['lab'] = int(lab)
1048 val_snippets.append('%(lab)s')
1049 except Exception:
1050 vals['lab'] = lab
1051 val_snippets.append('(SELECT pk_test_org FROM clin.v_test_orgs WHERE unit = %(lab)s)')
1052
1053
1054 cols.append('abbrev')
1055 val_snippets.append('%(abbrev)s')
1056 vals['abbrev'] = abbrev
1057
1058
1059 if unit is not None:
1060 cols.append('reference_unit')
1061 val_snippets.append('%(unit)s')
1062 vals['unit'] = unit
1063
1064
1065 if name is not None:
1066 cols.append('name')
1067 val_snippets.append('%(name)s')
1068 vals['name'] = name
1069
1070 col_clause = ', '.join(cols)
1071 val_clause = ', '.join(val_snippets)
1072 queries = [
1073 {'cmd': 'insert into clin.test_type (%s) values (%s)' % (col_clause, val_clause), 'args': vals},
1074 {'cmd': "select * from clin.v_test_types where pk_test_type = currval(pg_get_serial_sequence('clin.test_type', 'pk'))"}
1075 ]
1076 rows, idx = gmPG2.run_rw_queries(link_obj = link_obj, queries = queries, get_col_idx = True, return_data = True)
1077 ttype = cMeasurementType(row = {'pk_field': 'pk_test_type', 'data': rows[0], 'idx': idx})
1078
1079 return ttype
1080
1081
1082 -class cTestResult(gmBusinessDBObject.cBusinessDBObject):
1083 """Represents one test result."""
1084
1085 _cmd_fetch_payload = "select * from clin.v_test_results where pk_test_result = %s"
1086
1087 _cmds_store_payload = [
1088 """UPDATE clin.test_result SET
1089 clin_when = %(clin_when)s,
1090 narrative = nullif(trim(%(comment)s), ''),
1091 val_num = %(val_num)s,
1092 val_alpha = nullif(trim(%(val_alpha)s), ''),
1093 val_unit = nullif(trim(%(val_unit)s), ''),
1094 val_normal_min = %(val_normal_min)s,
1095 val_normal_max = %(val_normal_max)s,
1096 val_normal_range = nullif(trim(%(val_normal_range)s), ''),
1097 val_target_min = %(val_target_min)s,
1098 val_target_max = %(val_target_max)s,
1099 val_target_range = nullif(trim(%(val_target_range)s), ''),
1100 abnormality_indicator = nullif(trim(%(abnormality_indicator)s), ''),
1101 norm_ref_group = nullif(trim(%(norm_ref_group)s), ''),
1102 note_test_org = nullif(trim(%(note_test_org)s), ''),
1103 material = nullif(trim(%(material)s), ''),
1104 material_detail = nullif(trim(%(material_detail)s), ''),
1105 status = gm.nullify_empty_string(%(status)s),
1106 val_grouping = gm.nullify_empty_string(%(val_grouping)s),
1107 source_data = gm.nullify_empty_string(%(source_data)s),
1108 fk_intended_reviewer = %(pk_intended_reviewer)s,
1109 fk_encounter = %(pk_encounter)s,
1110 fk_episode = %(pk_episode)s,
1111 fk_type = %(pk_test_type)s,
1112 fk_request = %(pk_request)s
1113 WHERE
1114 pk = %(pk_test_result)s AND
1115 xmin = %(xmin_test_result)s
1116 RETURNING
1117 xmin AS xmin_test_result
1118 """
1119
1120 ]
1121
1122 _updatable_fields = [
1123 'clin_when',
1124 'comment',
1125 'val_num',
1126 'val_alpha',
1127 'val_unit',
1128 'val_normal_min',
1129 'val_normal_max',
1130 'val_normal_range',
1131 'val_target_min',
1132 'val_target_max',
1133 'val_target_range',
1134 'abnormality_indicator',
1135 'norm_ref_group',
1136 'note_test_org',
1137 'material',
1138 'material_detail',
1139 'status',
1140 'val_grouping',
1141 'source_data',
1142 'pk_intended_reviewer',
1143 'pk_encounter',
1144 'pk_episode',
1145 'pk_test_type',
1146 'pk_request'
1147 ]
1148
1149
1182
1183
1431
1432
1434 return (
1435 self._payload[self._idx['val_normal_min']] is not None
1436 ) or (
1437 self._payload[self._idx['val_normal_max']] is not None
1438 )
1439
1440 has_normal_min_or_max = property(_get_has_normal_min_or_max, lambda x:x)
1441
1442
1444 has_range_info = (
1445 self._payload[self._idx['val_normal_min']] is not None
1446 ) or (
1447 self._payload[self._idx['val_normal_max']] is not None
1448 )
1449 if has_range_info is False:
1450 return None
1451
1452 return '%s - %s' % (
1453 gmTools.coalesce(self._payload[self._idx['val_normal_min']], '?'),
1454 gmTools.coalesce(self._payload[self._idx['val_normal_max']], '?')
1455 )
1456
1457 normal_min_max = property(_get_normal_min_max, lambda x:x)
1458
1459
1486
1487 formatted_normal_range = property(_get_formatted_normal_range, lambda x:x)
1488
1489
1491 return (
1492 self._payload[self._idx['val_target_min']] is not None
1493 ) or (
1494 self._payload[self._idx['val_target_max']] is not None
1495 )
1496
1497 has_clinical_min_or_max = property(_get_has_clinical_min_or_max, lambda x:x)
1498
1499
1501 has_range_info = (
1502 self._payload[self._idx['val_target_min']] is not None
1503 ) or (
1504 self._payload[self._idx['val_target_max']] is not None
1505 )
1506 if has_range_info is False:
1507 return None
1508
1509 return '%s - %s' % (
1510 gmTools.coalesce(self._payload[self._idx['val_target_min']], '?'),
1511 gmTools.coalesce(self._payload[self._idx['val_target_max']], '?')
1512 )
1513
1514 clinical_min_max = property(_get_clinical_min_max, lambda x:x)
1515
1516
1543
1544 formatted_clinical_range = property(_get_formatted_clinical_range, lambda x:x)
1545
1546
1548 """Returns the closest test result which does have normal range information."""
1549 if self._payload[self._idx['val_normal_min']] is not None:
1550 return self
1551 if self._payload[self._idx['val_normal_max']] is not None:
1552 return self
1553 if self._payload[self._idx['val_normal_range']] is not None:
1554 return self
1555 cmd = """
1556 SELECT * from clin.v_test_results
1557 WHERE
1558 pk_type = %(pk_type)s
1559 AND
1560 val_unit = %(unit)s
1561 AND
1562 (
1563 (val_normal_min IS NOT NULL)
1564 OR
1565 (val_normal_max IS NOT NULL)
1566 OR
1567 (val_normal_range IS NOT NULL)
1568 )
1569 ORDER BY
1570 CASE
1571 WHEN clin_when > %(clin_when)s THEN clin_when - %(clin_when)s
1572 ELSE %(clin_when)s - clin_when
1573 END
1574 LIMIT 1"""
1575 args = {
1576 'pk_type': self._payload[self._idx['pk_test_type']],
1577 'unit': self._payload[self._idx['val_unit']],
1578 'clin_when': self._payload[self._idx['clin_when']]
1579 }
1580 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
1581 if len(rows) == 0:
1582 return None
1583 return cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': rows[0]})
1584
1585 temporally_closest_normal_range = property(_get_temporally_closest_normal_range, lambda x:x)
1586
1587
1647
1648 formatted_range = property(_get_formatted_range, lambda x:x)
1649
1650
1652 return cMeasurementType(aPK_obj = self._payload[self._idx['pk_test_type']])
1653
1654 test_type = property(_get_test_type, lambda x:x)
1655
1656
1658
1659 if self._payload[self._idx['is_technically_abnormal']] is False:
1660 return False
1661
1662 indicator = self._payload[self._idx['abnormality_indicator']]
1663 if indicator is not None:
1664 indicator = indicator.strip()
1665 if indicator != '':
1666 if indicator.strip('+') == '':
1667 return True
1668 if indicator.strip('-') == '':
1669 return False
1670
1671 if self._payload[self._idx['val_num']] is None:
1672 return None
1673
1674 target_max = self._payload[self._idx['val_target_max']]
1675 if target_max is not None:
1676 if target_max < self._payload[self._idx['val_num']]:
1677 return True
1678
1679 normal_max = self._payload[self._idx['val_normal_max']]
1680 if normal_max is not None:
1681 if normal_max < self._payload[self._idx['val_num']]:
1682 return True
1683 return None
1684
1685 is_considered_elevated = property(_get_is_considered_elevated, lambda x:x)
1686
1687
1689
1690 if self._payload[self._idx['is_technically_abnormal']] is False:
1691 return False
1692
1693 indicator = self._payload[self._idx['abnormality_indicator']]
1694 if indicator is not None:
1695 indicator = indicator.strip()
1696 if indicator != '':
1697 if indicator.strip('+') == '':
1698 return False
1699 if indicator.strip('-') == '':
1700 return True
1701
1702 if self._payload[self._idx['val_num']] is None:
1703 return None
1704
1705 target_min = self._payload[self._idx['val_target_min']]
1706 if target_min is not None:
1707 if target_min > self._payload[self._idx['val_num']]:
1708 return True
1709
1710 normal_min = self._payload[self._idx['val_normal_min']]
1711 if normal_min is not None:
1712 if normal_min > self._payload[self._idx['val_num']]:
1713 return True
1714 return None
1715
1716 is_considered_lowered = property(_get_is_considered_lowered, lambda x:x)
1717
1718
1727
1728 is_considered_abnormal = property(_get_is_considered_abnormal, lambda x:x)
1729
1730
1732 """Parse reference range from string.
1733
1734 Note: does NOT save the result.
1735 """
1736 ref_range = ref_range.strip().replace(' ', '')
1737
1738 is_range = regex.match('-{0,1}\d+[.,]{0,1}\d*--{0,1}\d+[.,]{0,1}\d*$', ref_range, regex.UNICODE)
1739 if is_range is not None:
1740 min_val = regex.match('-{0,1}\d+[.,]{0,1}\d*-', ref_range, regex.UNICODE).group(0).rstrip('-')
1741 success, min_val = gmTools.input2decimal(min_val)
1742 max_val = (regex.search('--{0,1}\d+[.,]{0,1}\d*$', ref_range, regex.UNICODE).group(0))[1:]
1743 success, max_val = gmTools.input2decimal(max_val)
1744 self['val_normal_min'] = min_val
1745 self['val_normal_max'] = max_val
1746 return
1747
1748 if ref_range.startswith('<'):
1749 is_range = regex.match('<\d+[.,]{0,1}\d*$', ref_range, regex.UNICODE)
1750 if is_range is not None:
1751 max_val = ref_range[1:]
1752 success, max_val = gmTools.input2decimal(max_val)
1753 self['val_normal_min'] = 0
1754 self['val_normal_max'] = max_val
1755 return
1756
1757 if ref_range.startswith('<-'):
1758 is_range = regex.match('<-\d+[.,]{0,1}\d*$', ref_range, regex.UNICODE)
1759 if is_range is not None:
1760 max_val = ref_range[1:]
1761 success, max_val = gmTools.input2decimal(max_val)
1762 self['val_normal_min'] = None
1763 self['val_normal_max'] = max_val
1764 return
1765
1766 if ref_range.startswith('>'):
1767 is_range = regex.match('>\d+[.,]{0,1}\d*$', ref_range, regex.UNICODE)
1768 if is_range is not None:
1769 min_val = ref_range[1:]
1770 success, min_val = gmTools.input2decimal(min_val)
1771 self['val_normal_min'] = min_val
1772 self['val_normal_max'] = None
1773 return
1774
1775 if ref_range.startswith('>-'):
1776 is_range = regex.match('>-\d+[.,]{0,1}\d*$', ref_range, regex.UNICODE)
1777 if is_range is not None:
1778 min_val = ref_range[1:]
1779 success, min_val = gmTools.input2decimal(min_val)
1780 self['val_normal_min'] = min_val
1781 self['val_normal_max'] = 0
1782 return
1783
1784 self['val_normal_range'] = ref_range
1785 return
1786
1787 reference_range = property(lambda x:x, _set_reference_range)
1788
1789
1826
1827 formatted_abnormality_indicator = property(_get_formatted_abnormality_indicator, lambda x:x)
1828
1829
1831 if self._payload[self._idx['val_alpha']] is None:
1832 return False
1833 lines = gmTools.strip_empty_lines(text = self._payload[self._idx['val_alpha']], eol = '\n', return_list = True)
1834 if len(lines) > 4:
1835 return True
1836 return False
1837
1838 is_long_text = property(_get_is_long_text, lambda x:x)
1839
1840
1842 if self._payload[self._idx['val_alpha']] is None:
1843 return None
1844 val = self._payload[self._idx['val_alpha']].lstrip()
1845 if val[0] == '<':
1846 factor = decimal.Decimal(0.5)
1847 val = val[1:]
1848 elif val[0] == '>':
1849 factor = 2
1850 val = val[1:]
1851 else:
1852 return None
1853 success, val = gmTools.input2decimal(initial = val)
1854 if not success:
1855 return None
1856 return val * factor
1857
1858 estimate_numeric_value_from_alpha = property(_get_estimate_numeric_value_from_alpha, lambda x:x)
1859
1860
1861 - def set_review(self, technically_abnormal=None, clinically_relevant=None, comment=None, make_me_responsible=False):
1862
1863
1864 if self._payload[self._idx['reviewed']]:
1865 self.__change_existing_review (
1866 technically_abnormal = technically_abnormal,
1867 clinically_relevant = clinically_relevant,
1868 comment = comment
1869 )
1870 else:
1871
1872
1873 if technically_abnormal is None:
1874 if clinically_relevant is None:
1875 comment = gmTools.none_if(comment, '', strip_string = True)
1876 if comment is None:
1877 if make_me_responsible is False:
1878 return True
1879 self.__set_new_review (
1880 technically_abnormal = technically_abnormal,
1881 clinically_relevant = clinically_relevant,
1882 comment = comment
1883 )
1884
1885 if make_me_responsible is True:
1886 cmd = "SELECT pk FROM dem.staff WHERE db_user = current_user"
1887 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd}])
1888 self['pk_intended_reviewer'] = rows[0][0]
1889 self.save_payload()
1890 return
1891
1892 self.refetch_payload()
1893
1894
1895 - def get_adjacent_results(self, desired_earlier_results=1, desired_later_results=1, max_offset=None):
1896
1897 if desired_earlier_results < 1:
1898 raise ValueError('<desired_earlier_results> must be > 0')
1899
1900 if desired_later_results < 1:
1901 raise ValueError('<desired_later_results> must be > 0')
1902
1903 args = {
1904 'pat': self._payload[self._idx['pk_patient']],
1905 'ttyp': self._payload[self._idx['pk_test_type']],
1906 'tloinc': self._payload[self._idx['loinc_tt']],
1907 'mtyp': self._payload[self._idx['pk_meta_test_type']],
1908 'mloinc': self._payload[self._idx['loinc_meta']],
1909 'when': self._payload[self._idx['clin_when']],
1910 'offset': max_offset
1911 }
1912 WHERE = '((pk_test_type = %(ttyp)s) OR (loinc_tt = %(tloinc)s))'
1913 WHERE_meta = '((pk_meta_test_type = %(mtyp)s) OR (loinc_meta = %(mloinc)s))'
1914 if max_offset is not None:
1915 WHERE = WHERE + ' AND (clin_when BETWEEN (%(when)s - %(offset)s) AND (%(when)s + %(offset)s))'
1916 WHERE_meta = WHERE_meta + ' AND (clin_when BETWEEN (%(when)s - %(offset)s) AND (%(when)s + %(offset)s))'
1917
1918 SQL = """
1919 SELECT * FROM clin.v_test_results
1920 WHERE
1921 pk_patient = %%(pat)s
1922 AND
1923 clin_when %s %%(when)s
1924 AND
1925 %s
1926 ORDER BY clin_when
1927 LIMIT %s"""
1928
1929
1930 earlier_results = []
1931
1932 cmd = SQL % ('<', WHERE, desired_earlier_results)
1933 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
1934 if len(rows) > 0:
1935 earlier_results.extend([ cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': r}) for r in rows ])
1936
1937 missing_results = desired_earlier_results - len(earlier_results)
1938 if missing_results > 0:
1939 cmd = SQL % ('<', WHERE_meta, missing_results)
1940 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
1941 if len(rows) > 0:
1942 earlier_results.extend([ cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': r}) for r in rows ])
1943
1944
1945 later_results = []
1946
1947 cmd = SQL % ('>', WHERE, desired_later_results)
1948 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
1949 if len(rows) > 0:
1950 later_results.extend([ cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': r}) for r in rows ])
1951
1952 missing_results = desired_later_results - len(later_results)
1953 if missing_results > 0:
1954 cmd = SQL % ('>', WHERE_meta, missing_results)
1955 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
1956 if len(rows) > 0:
1957 later_results.extend([ cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': r}) for r in rows ])
1958
1959 return earlier_results, later_results
1960
1961
1962
1963
1964 - def __set_new_review(self, technically_abnormal=None, clinically_relevant=None, comment=None):
1965 """Add a review to a row.
1966
1967 - if technically abnormal is not provided/None it will be set
1968 to True if the lab's indicator has a meaningful value
1969 - if clinically relevant is not provided/None it is set to
1970 whatever technically abnormal is
1971 """
1972 if technically_abnormal is None:
1973 technically_abnormal = False
1974 if self._payload[self._idx['abnormality_indicator']] is not None:
1975 if self._payload[self._idx['abnormality_indicator']].strip() != '':
1976 technically_abnormal = True
1977
1978 if clinically_relevant is None:
1979 clinically_relevant = technically_abnormal
1980
1981 cmd = """
1982 INSERT INTO clin.reviewed_test_results (
1983 fk_reviewed_row,
1984 is_technically_abnormal,
1985 clinically_relevant,
1986 comment
1987 ) VALUES (
1988 %(pk)s,
1989 %(abnormal)s,
1990 %(relevant)s,
1991 gm.nullify_empty_string(%(cmt)s)
1992 )"""
1993 args = {
1994 'pk': self._payload[self._idx['pk_test_result']],
1995 'abnormal': technically_abnormal,
1996 'relevant': clinically_relevant,
1997 'cmt': comment
1998 }
1999
2000 gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
2001
2002
2004 """Change a review on a row.
2005
2006 - if technically abnormal/clinically relevant are
2007 None they are not set
2008 """
2009 args = {
2010 'pk_result': self._payload[self._idx['pk_test_result']],
2011 'abnormal': technically_abnormal,
2012 'relevant': clinically_relevant,
2013 'cmt': comment
2014 }
2015
2016 set_parts = [
2017 'fk_reviewer = (SELECT pk FROM dem.staff WHERE db_user = current_user)',
2018 'comment = gm.nullify_empty_string(%(cmt)s)'
2019 ]
2020
2021 if technically_abnormal is not None:
2022 set_parts.append('is_technically_abnormal = %(abnormal)s')
2023
2024 if clinically_relevant is not None:
2025 set_parts.append('clinically_relevant = %(relevant)s')
2026
2027 cmd = """
2028 UPDATE clin.reviewed_test_results SET
2029 %s
2030 WHERE
2031 fk_reviewed_row = %%(pk_result)s
2032 """ % ',\n '.join(set_parts)
2033
2034 gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
2035
2036
2037 -def get_test_results(pk_patient=None, encounters=None, episodes=None, order_by=None, return_pks=False):
2038
2039 where_parts = []
2040
2041 if pk_patient is not None:
2042 where_parts.append('pk_patient = %(pat)s')
2043 args = {'pat': pk_patient}
2044
2045
2046
2047
2048
2049 if encounters is not None:
2050 where_parts.append('pk_encounter IN %(encs)s')
2051 args['encs'] = tuple(encounters)
2052
2053 if episodes is not None:
2054 where_parts.append('pk_episode IN %(epis)s')
2055 args['epis'] = tuple(episodes)
2056
2057 if order_by is None:
2058 order_by = ''
2059 else:
2060 order_by = 'ORDER BY %s' % order_by
2061
2062 cmd = """
2063 SELECT * FROM clin.v_test_results
2064 WHERE %s
2065 %s
2066 """ % (
2067 ' AND '.join(where_parts),
2068 order_by
2069 )
2070 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
2071 if return_pks:
2072 return [ r['pk_test_result'] for r in rows ]
2073 tests = [ cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': r}) for r in rows ]
2074 return tests
2075
2076
2078
2079 if order_by is None:
2080 order_by = ''
2081 else:
2082 order_by = 'ORDER BY %s' % order_by
2083
2084 args = {
2085 'pat': pk_patient,
2086 'pk_pnl': pk_panel
2087 }
2088
2089 if group_by_meta_type:
2090
2091
2092
2093 cmd = """
2094 SELECT c_vtr.*
2095 FROM (
2096 -- max(clin_when) per test_type-in-panel for patient
2097 SELECT
2098 pk_meta_test_type,
2099 MAX(clin_when) AS max_clin_when
2100 FROM clin.v_test_results
2101 WHERE
2102 pk_patient = %(pat)s
2103 AND
2104 pk_meta_test_type IS DISTINCT FROM NULL
2105 AND
2106 pk_test_type IN (
2107 (SELECT c_vtt4tp.pk_test_type FROM clin.v_test_types4test_panel c_vtt4tp WHERE c_vtt4tp.pk_test_panel = %(pk_pnl)s)
2108 )
2109 GROUP BY pk_meta_test_type
2110 ) AS latest_results
2111 INNER JOIN clin.v_test_results c_vtr ON
2112 c_vtr.pk_meta_test_type = latest_results.pk_meta_test_type
2113 AND
2114 c_vtr.clin_when = latest_results.max_clin_when
2115
2116 UNION ALL
2117
2118 SELECT c_vtr.*
2119 FROM (
2120 -- max(clin_when) per test_type-in-panel for patient
2121 SELECT
2122 pk_test_type,
2123 MAX(clin_when) AS max_clin_when
2124 FROM clin.v_test_results
2125 WHERE
2126 pk_patient = %(pat)s
2127 AND
2128 pk_meta_test_type IS NULL
2129 AND
2130 pk_test_type IN (
2131 (SELECT c_vtt4tp.pk_test_type FROM clin.v_test_types4test_panel c_vtt4tp WHERE c_vtt4tp.pk_test_panel = %(pk_pnl)s)
2132 )
2133 GROUP BY pk_test_type
2134 ) AS latest_results
2135 INNER JOIN clin.v_test_results c_vtr ON
2136 c_vtr.pk_test_type = latest_results.pk_test_type
2137 AND
2138 c_vtr.clin_when = latest_results.max_clin_when
2139 """
2140 else:
2141
2142
2143
2144 cmd = """
2145 SELECT c_vtr.*
2146 FROM (
2147 -- max(clin_when) per test_type-in-panel for patient
2148 SELECT
2149 pk_test_type,
2150 MAX(clin_when) AS max_clin_when
2151 FROM clin.v_test_results
2152 WHERE
2153 pk_patient = %(pat)s
2154 AND
2155 pk_test_type IN (
2156 (SELECT c_vtt4tp.pk_test_type FROM clin.v_test_types4test_panel c_vtt4tp WHERE c_vtt4tp.pk_test_panel = %(pk_pnl)s)
2157 )
2158 GROUP BY pk_test_type
2159 ) AS latest_results
2160 -- this INNER join makes certain we do not expand
2161 -- the row selection beyond the patient's rows
2162 -- which we constrained to inside the SELECT
2163 -- producing "latest_results"
2164 INNER JOIN clin.v_test_results c_vtr ON
2165 c_vtr.pk_test_type = latest_results.pk_test_type
2166 AND
2167 c_vtr.clin_when = latest_results.max_clin_when
2168 """
2169 cmd += order_by
2170 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
2171 return [ cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': r}) for r in rows ]
2172
2173
2175
2176 if None not in [test_type, loinc]:
2177 raise ValueError('either <test_type> or <loinc> must be None')
2178
2179 args = {
2180 'pat': patient,
2181 'ttyp': test_type,
2182 'loinc': loinc,
2183 'ts': timestamp,
2184 'intv': tolerance_interval
2185 }
2186
2187 where_parts = ['pk_patient = %(pat)s']
2188 if test_type is not None:
2189 where_parts.append('pk_test_type = %(ttyp)s')
2190 elif loinc is not None:
2191 where_parts.append('((loinc_tt IN %(loinc)s) OR (loinc_meta IN %(loinc)s))')
2192 args['loinc'] = tuple(loinc)
2193
2194 if tolerance_interval is None:
2195 where_parts.append('clin_when = %(ts)s')
2196 else:
2197 where_parts.append('clin_when between (%(ts)s - %(intv)s::interval) AND (%(ts)s + %(intv)s::interval)')
2198
2199 cmd = """
2200 SELECT * FROM clin.v_test_results
2201 WHERE
2202 %s
2203 ORDER BY
2204 abs(extract(epoch from age(clin_when, %%(ts)s)))
2205 LIMIT 1""" % ' AND '.join(where_parts)
2206
2207 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
2208 if len(rows) == 0:
2209 return None
2210
2211 return cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': rows[0]})
2212
2213
2215
2216 args = {
2217 'pat': patient,
2218 'ts': timestamp
2219 }
2220
2221 where_parts = [
2222 'pk_patient = %(pat)s',
2223 "date_trunc('day'::text, clin_when) = date_trunc('day'::text, %(ts)s)"
2224 ]
2225
2226 cmd = """
2227 SELECT * FROM clin.v_test_results
2228 WHERE
2229 %s
2230 ORDER BY
2231 val_grouping,
2232 abbrev_tt,
2233 clin_when DESC
2234 """ % ' AND '.join(where_parts)
2235 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
2236 return [ cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': r}) for r in rows ]
2237
2238
2240 args = {'pk_issue': pk_health_issue}
2241 where_parts = ['pk_health_issue = %(pk_issue)s']
2242 cmd = """
2243 SELECT * FROM clin.v_test_results
2244 WHERE %s
2245 ORDER BY
2246 val_grouping,
2247 abbrev_tt,
2248 clin_when DESC
2249 """ % ' AND '.join(where_parts)
2250 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
2251 return [ cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': r}) for r in rows ]
2252
2253
2255 args = {'pk_epi': pk_episode}
2256 where_parts = ['pk_episode = %(pk_epi)s']
2257 cmd = """
2258 SELECT * FROM clin.v_test_results
2259 WHERE %s
2260 ORDER BY
2261 val_grouping,
2262 abbrev_tt,
2263 clin_when DESC
2264 """ % ' AND '.join(where_parts)
2265 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
2266 return [ cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': r}) for r in rows ]
2267
2268
2270 """Get N most recent results *among* a list of tests selected by LOINC.
2271
2272 1) get test types with LOINC (or meta type LOINC) in the group of <loincs>
2273 2) from these get the test results for <patient> within the given <max_age>
2274 3) from these return "the N=<max_no_of_results> most recent ones" or "None"
2275
2276 <loinc> must be a list or tuple or set, NOT a single string
2277 <max_age> must be a string holding a PG interval or else a pydt interval
2278 """
2279 assert (max_no_of_results > 0), '<max_no_of_results> must be >0'
2280
2281 args = {'pat': patient, 'loincs': tuple(loincs)}
2282 if max_age is None:
2283 max_age_cond = ''
2284 else:
2285 max_age_cond = 'AND clin_when > (now() - %(max_age)s::interval)'
2286 args['max_age'] = max_age
2287 cmd = """
2288 SELECT * FROM (
2289 SELECT DISTINCT ON (pk_test_result) *
2290 FROM clin.v_test_results
2291 WHERE
2292 pk_patient = %%(pat)s
2293 AND
2294 unified_loinc IN %%(loincs)s
2295 %s
2296 ) AS distinct_results
2297 ORDER BY
2298 clin_when DESC
2299 LIMIT %s""" % (
2300 max_age_cond,
2301 max_no_of_results
2302 )
2303 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
2304 if len(rows) > 0:
2305 return [ cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': r}) for r in rows ]
2306
2307 if not consider_indirect_matches:
2308 return []
2309
2310 cmd = """
2311 -- get the test results
2312 SELECT * FROM clin.v_test_results c_vtr
2313 WHERE
2314 -- for this <patient>
2315 pk_patient = %%(pat)s
2316 AND
2317 -- 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)
2318 unified_loinc IS NULL
2319 AND
2320 -- with these meta test types (= results for the explicit equivalance group)
2321 c_vtr.pk_meta_test_type IN (
2322 -- get the meta test types for those types
2323 SELECT pk_meta_test_type
2324 FROM clin.v_test_types
2325 WHERE
2326 pk_meta_test_type IS NOT NULL
2327 AND
2328 (-- retrieve test types which have .LOINC in <loincs>
2329 (loinc IN %%(loincs)s)
2330 OR
2331 (loinc_meta IN %%(loincs)s)
2332 )
2333 AND
2334 -- but no result for <patient>
2335 pk_test_type NOT IN (
2336 select pk_test_type from clin.v_test_results WHERE pk_patient = %%(pat)s
2337 ) %s
2338 )
2339 -- return the N most resent result
2340 ORDER BY clin_when DESC
2341 LIMIT %s
2342 """ % (
2343 max_age_cond,
2344 max_no_of_results
2345 )
2346 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
2347 return [ cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': r}) for r in rows ]
2348
2349
2351 """Get N most recent results for *one* defined test type."""
2352
2353 assert (test_type is not None), '<test_type> must not be None'
2354 assert (max_no_of_results > 0), '<max_no_of_results> must be > 0'
2355
2356 args = {'pat': patient, 'ttyp': test_type}
2357 where_parts = ['pk_patient = %(pat)s']
2358 where_parts.append('pk_test_type = %(ttyp)s')
2359 cmd = """
2360 SELECT * FROM clin.v_test_results
2361 WHERE
2362 %s
2363 ORDER BY clin_when DESC
2364 LIMIT %s""" % (
2365 ' AND '.join(where_parts),
2366 max_no_of_results
2367 )
2368 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
2369 return [ cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': r}) for r in rows ]
2370
2371
2372 _SQL_most_recent_result_for_test_types = """
2373 -- return the one most recent result for each of a list of test types
2374 -- without regard to whether they belong to a meta test type
2375 SELECT * FROM (
2376 SELECT
2377 *,
2378 MAX(clin_when) OVER relevant_tests AS max_clin_when
2379 FROM
2380 clin.v_test_results
2381 WHERE
2382 %s
2383 WINDOW relevant_tests AS (PARTITION BY pk_patient, pk_test_type)
2384 ) AS windowed_tests
2385 WHERE
2386 clin_when = max_clin_when
2387 %s
2388 """
2389
2390 _SQL_most_recent_result_for_test_types_without_meta_type = """
2391 -- return the one most recent result for each of a list of test types
2392 -- none of which may belong to a meta test type
2393 SELECT * FROM (
2394 SELECT
2395 *,
2396 MAX(clin_when) OVER relevant_tests AS max_clin_when
2397 FROM
2398 clin.v_test_results
2399 WHERE
2400 pk_meta_test_type IS NULL
2401 AND
2402 %s
2403 WINDOW relevant_tests AS (PARTITION BY pk_patient, pk_test_type)
2404 ) AS windowed_tests
2405 WHERE
2406 clin_when = max_clin_when
2407 """
2408
2409 _SQL_most_recent_result_for_test_types_by_meta_type = """
2410 -- return the one most recent result for each of a list of meta test types
2411 -- derived from a list of test types
2412 SELECT * FROM (
2413 SELECT
2414 *,
2415 MAX(clin_when) OVER relevant_tests AS max_clin_when
2416 FROM
2417 clin.v_test_results
2418 WHERE
2419 pk_meta_test_type IS NOT NULL
2420 AND
2421 %s
2422 WINDOW relevant_tests AS (PARTITION BY pk_patient, pk_meta_test_type)
2423 ) AS windowed_tests
2424 WHERE
2425 clin_when = max_clin_when
2426 """
2427
2429 """Return the one most recent result for *each* of a list of test types."""
2430
2431 where_parts = ['pk_patient = %(pat)s']
2432 args = {'pat': pk_patient}
2433
2434 if pk_test_types is not None:
2435 where_parts.append('pk_test_type IN %(ttyps)s')
2436 args['ttyps'] = tuple(pk_test_types)
2437
2438 order_by = 'ORDER BY clin_when DESC' if order_by is None else 'ORDER BY %s' % order_by
2439
2440 if consider_meta_type:
2441 cmd = 'SELECT * FROM ((%s) UNION ALL (%s)) AS result_union %s' % (
2442 _SQL_most_recent_result_for_test_types_without_meta_type % ' AND '.join(where_parts),
2443 _SQL_most_recent_result_for_test_types_by_meta_type % ' AND '.join(where_parts),
2444 order_by
2445 )
2446 else:
2447 cmd = _SQL_most_recent_result_for_test_types % (
2448 ' AND '.join(where_parts),
2449 order_by
2450 )
2451 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
2452 if return_pks:
2453 return [ r['pk_test_result'] for r in rows ]
2454 return [ cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': r}) for r in rows ]
2455
2456
2458 """Get N most recent results for a given patient."""
2459
2460 if no_of_results < 1:
2461 raise ValueError('<no_of_results> must be > 0')
2462
2463 args = {'pat': patient}
2464 cmd = """
2465 SELECT * FROM clin.v_test_results
2466 WHERE
2467 pk_patient = %%(pat)s
2468 ORDER BY clin_when DESC
2469 LIMIT %s""" % no_of_results
2470 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
2471 return [ cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': r}) for r in rows ]
2472
2473
2475
2476 if None not in [test_type, loinc]:
2477 raise ValueError('either <test_type> or <loinc> must be None')
2478
2479 args = {
2480 'pat': patient,
2481 'ttyp': test_type,
2482 'loinc': loinc
2483 }
2484
2485 where_parts = ['pk_patient = %(pat)s']
2486 if test_type is not None:
2487 where_parts.append('pk_test_type = %(ttyp)s')
2488 elif loinc is not None:
2489 where_parts.append('((loinc_tt IN %(loinc)s) OR (loinc_meta IN %(loinc)s))')
2490 args['loinc'] = tuple(loinc)
2491
2492 cmd = """
2493 SELECT * FROM clin.v_test_results
2494 WHERE
2495 %s
2496 ORDER BY clin_when
2497 LIMIT 1""" % ' AND '.join(where_parts)
2498 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
2499 if len(rows) == 0:
2500 return None
2501
2502 return cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': rows[0]})
2503
2504
2506 try:
2507 pk = int(result)
2508 except (TypeError, AttributeError):
2509 pk = result['pk_test_result']
2510
2511 cmd = 'DELETE FROM clin.test_result WHERE pk = %(pk)s'
2512 gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': {'pk': pk}}])
2513
2514
2515 -def create_test_result(encounter=None, episode=None, type=None, intended_reviewer=None, val_num=None, val_alpha=None, unit=None, link_obj=None):
2516
2517 cmd1 = """
2518 INSERT INTO clin.test_result (
2519 fk_encounter,
2520 fk_episode,
2521 fk_type,
2522 fk_intended_reviewer,
2523 val_num,
2524 val_alpha,
2525 val_unit
2526 ) VALUES (
2527 %(enc)s,
2528 %(epi)s,
2529 %(type)s,
2530 %(rev)s,
2531 %(v_num)s,
2532 %(v_alpha)s,
2533 %(unit)s
2534 )
2535 """
2536 cmd2 = "SELECT * from clin.v_test_results WHERE pk_test_result = currval(pg_get_serial_sequence('clin.test_result', 'pk'))"
2537 args = {
2538 'enc': encounter,
2539 'epi': episode,
2540 'type': type,
2541 'rev': intended_reviewer,
2542 'v_num': val_num,
2543 'v_alpha': val_alpha,
2544 'unit': unit
2545 }
2546 rows, idx = gmPG2.run_rw_queries (
2547 link_obj = link_obj,
2548 queries = [
2549 {'cmd': cmd1, 'args': args},
2550 {'cmd': cmd2}
2551 ],
2552 return_data = True,
2553 get_col_idx = True
2554 )
2555 tr = cTestResult(row = {
2556 'pk_field': 'pk_test_result',
2557 'idx': idx,
2558 'data': rows[0]
2559 })
2560 return tr
2561
2562
2573
2574
2575 -def __tests2latex_minipage(results=None, width='1.5cm', show_time=False, show_range=True):
2576
2577 if len(results) == 0:
2578 return '\\begin{minipage}{%s} \\end{minipage}' % width
2579
2580 lines = []
2581 for t in results:
2582
2583 tmp = ''
2584
2585 if show_time:
2586 tmp += '{\\tiny (%s)} ' % t['clin_when'].strftime('%H:%M')
2587
2588 tmp += '%.8s' % t['unified_val']
2589
2590 lines.append(tmp)
2591 tmp = ''
2592
2593 if show_range:
2594 has_range = (
2595 t['unified_target_range'] is not None
2596 or
2597 t['unified_target_min'] is not None
2598 or
2599 t['unified_target_max'] is not None
2600 )
2601 if has_range:
2602 if t['unified_target_range'] is not None:
2603 tmp += '{\\tiny %s}' % t['unified_target_range']
2604 else:
2605 tmp += '{\\tiny %s}' % (
2606 gmTools.coalesce(t['unified_target_min'], '- ', '%s - '),
2607 gmTools.coalesce(t['unified_target_max'], '', '%s')
2608 )
2609 lines.append(tmp)
2610
2611 return '\\begin{minipage}{%s} \\begin{flushright} %s \\end{flushright} \\end{minipage}' % (width, ' \\\\ '.join(lines))
2612
2613
2615
2616 if len(results) == 0:
2617 return ''
2618
2619 lines = []
2620 for t in results:
2621
2622 tmp = ''
2623
2624 if show_time:
2625 tmp += '\\tiny %s ' % t['clin_when'].strftime('%H:%M')
2626
2627 tmp += '\\normalsize %.8s' % t['unified_val']
2628
2629 lines.append(tmp)
2630 tmp = '\\tiny %s' % gmTools.coalesce(t['val_unit'], '', '%s ')
2631
2632 if not show_range:
2633 lines.append(tmp)
2634 continue
2635
2636 has_range = (
2637 t['unified_target_range'] is not None
2638 or
2639 t['unified_target_min'] is not None
2640 or
2641 t['unified_target_max'] is not None
2642 )
2643
2644 if not has_range:
2645 lines.append(tmp)
2646 continue
2647
2648 if t['unified_target_range'] is not None:
2649 tmp += '[%s]' % t['unified_target_range']
2650 else:
2651 tmp += '[%s%s]' % (
2652 gmTools.coalesce(t['unified_target_min'], '--', '%s--'),
2653 gmTools.coalesce(t['unified_target_max'], '', '%s')
2654 )
2655 lines.append(tmp)
2656
2657 return ' \\\\ '.join(lines)
2658
2659
2735
2736
2738
2739 sandbox_dir = os.path.join(gmTools.gmPaths().tmp_dir, 'gplot')
2740 if not gmTools.mkdir(sandbox_dir):
2741 sandbox_dir = gmTools.mk_sandbox_dir(prefix = 'gm2gpl-')
2742 _log.debug('sandbox directory: [%s]', sandbox_dir)
2743 if filename is None:
2744 filename = gmTools.get_unique_filename(prefix = 'gm2gpl-', suffix = '.dat', tmp_dir = sandbox_dir)
2745
2746
2747 series = {}
2748 for r in results:
2749 try:
2750 series[r['unified_name']].append(r)
2751 except KeyError:
2752 series[r['unified_name']] = [r]
2753
2754 conf_name = '%s.conf' % filename
2755 gplot_conf = io.open(conf_name, mode = 'wt', encoding = 'utf8')
2756 gplot_conf.write('# settings for stacked multiplot layouts:\n')
2757 sub_title = _('plotted %s (GNUmed v%s)') % (
2758 gmDateTime.pydt_now_here().strftime('%Y %b %d %H:%M'),
2759 _cfg.get(option = 'client_version')
2760 )
2761 if patient is None:
2762 plot_title = sub_title
2763 else:
2764 plot_title = '%s - %s\\n%s' % (
2765 patient.get_description_gender(with_nickname = False).strip(),
2766 patient.get_formatted_dob(format = '%Y %b %d', none_string = _('unknown DOB'), honor_estimation = True),
2767 sub_title
2768 )
2769 gplot_conf.write('multiplot_title = "%s"\n' % plot_title)
2770 gplot_conf.write('multiplot_no_of_tests = %s # number of index blocks (resp. test types)\n' % len(series))
2771 gplot_conf.write('array multiplot_y_labels[multiplot_no_of_tests] # list for ylabels suitable for stacked multiplots\n')
2772 gplot_conf.write('\n')
2773 gplot_conf.write('# settings for individual plots, stacked or not:\n')
2774
2775 gplot_data = io.open(filename, mode = 'wt', encoding = 'utf8')
2776 gplot_data.write('# -------------------------------------------------------------\n')
2777 gplot_data.write('# GNUmed test results export for Gnuplot plotting\n')
2778 gplot_data.write('# -------------------------------------------------------------\n')
2779 gplot_data.write('# first line of each index: test type abbreviation & name,\n')
2780 gplot_data.write('# can be used as title for plots: set key ... autotitle columnheader\n')
2781 gplot_data.write('#\n')
2782 gplot_data.write('# Columns in each index:\n')
2783 gplot_data.write('# 1 - clin_when at full precision\n')
2784 gplot_data.write('# set timefmt "%Y-%m-%d_%H:%M"\n')
2785 gplot_data.write('# timecolumn(1, "%Y-%m-%d_%H:%M")\n')
2786 gplot_data.write('# 2 - value\n')
2787 gplot_data.write('# 3 - unit\n')
2788 gplot_data.write('# 4 - unified (target or normal) range: lower bound\n')
2789 gplot_data.write('# 5 - unified (target or normal) range: upper bound\n')
2790 gplot_data.write('# 6 - normal range: lower bound\n')
2791 gplot_data.write('# 7 - normal range: upper bound\n')
2792 gplot_data.write('# 8 - target range: lower bound\n')
2793 gplot_data.write('# 9 - target range: upper bound\n')
2794 gplot_data.write('# 10 - clin_when formatted into string (say, as x-axis tic label)\n')
2795 gplot_data.write('#\n')
2796 gplot_data.write('# index rows are NOT sorted by clin_when, so plotting\n')
2797 gplot_data.write('# with lined styles will make the lines go all over\n')
2798 gplot_data.write('# -------------------------------------------------------------\n')
2799 gplot_data.write('#\n')
2800 gplot_data.write('# the file <%s.conf>\n' % filename)
2801 gplot_data.write('# will contain various gnuplot settings specific to this plot,\n')
2802 gplot_data.write('# such as <ylabel>, <y2label>, <title>,\n')
2803 gplot_data.write('# there will also be settings suitable for stacked multiplots\n')
2804 gplot_data.write('# -------------------------------------------------------------\n')
2805
2806 series_keys = list(series.keys())
2807 for test_type_idx in range(len(series_keys)):
2808 test_type = series_keys[test_type_idx]
2809 if len(series[test_type]) == 0:
2810 continue
2811 result = series[test_type][0]
2812 if test_type_idx == 0:
2813 gplot_conf.write('set title "%s" enhanced\n' % plot_title)
2814 gplot_conf.write('\n')
2815 gplot_conf.write('set ylabel "%s"\n' % result['unified_name'])
2816 elif test_type_idx == 1:
2817 gplot_conf.write('set y2label "%s"\n' % result['unified_name'])
2818 gplot_conf.write('multiplot_y_labels[%s] = "%s (%s)"\n' % (test_type_idx + 1, result['unified_name'], result['unified_abbrev']))
2819 title = '%s (%s)' % (
2820 result['unified_abbrev'],
2821 result['unified_name']
2822 )
2823 gplot_data.write('\n\n"%s" "%s"\n' % (title, title))
2824 prev_date = None
2825 prev_year = None
2826 for result in series[test_type]:
2827 curr_date = gmDateTime.pydt_strftime(result['clin_when'], '%Y-%m-%d', 'utf8', gmDateTime.acc_days)
2828 if curr_date == prev_date:
2829 gplot_data.write('\n# %s\n' % _('blank line inserted to allow for discontinued line drawing of same-day values'))
2830 if show_year:
2831 if result['clin_when'].year == prev_year:
2832 when_template = '%b %d %H:%M'
2833 else:
2834 when_template = '%b %d %H:%M (%Y)'
2835 prev_year = result['clin_when'].year
2836 else:
2837 when_template = '%b %d'
2838 val = result['val_num']
2839 if val is None:
2840 val = result.estimate_numeric_value_from_alpha
2841 if val is None:
2842 continue
2843 gplot_data.write ('%s %s "%s" %s %s %s %s %s %s "%s"\n' % (
2844
2845 gmDateTime.pydt_strftime(result['clin_when'], '%Y-%m-%d_%H:%M', 'utf8', gmDateTime.acc_minutes),
2846 val,
2847 gmTools.coalesce(result['val_unit'], '"<?>"'),
2848 gmTools.coalesce(result['unified_target_min'], '"<?>"'),
2849 gmTools.coalesce(result['unified_target_max'], '"<?>"'),
2850 gmTools.coalesce(result['val_normal_min'], '"<?>"'),
2851 gmTools.coalesce(result['val_normal_max'], '"<?>"'),
2852 gmTools.coalesce(result['val_target_min'], '"<?>"'),
2853 gmTools.coalesce(result['val_target_max'], '"<?>"'),
2854 gmDateTime.pydt_strftime (
2855 result['clin_when'],
2856 format = when_template,
2857 accuracy = gmDateTime.acc_minutes
2858 )
2859 ))
2860 prev_date = curr_date
2861 gplot_data.close()
2862 gplot_conf.close()
2863
2864 return filename
2865
2866
2867 -class cLabResult(gmBusinessDBObject.cBusinessDBObject):
2868 """Represents one lab result."""
2869
2870 _cmd_fetch_payload = """
2871 select *, xmin_test_result from v_results4lab_req
2872 where pk_result=%s"""
2873 _cmds_lock_rows_for_update = [
2874 """select 1 from test_result where pk=%(pk_result)s and xmin=%(xmin_test_result)s for update"""
2875 ]
2876 _cmds_store_payload = [
2877 """update test_result set
2878 clin_when = %(val_when)s,
2879 narrative = %(progress_note_result)s,
2880 fk_type = %(pk_test_type)s,
2881 val_num = %(val_num)s::numeric,
2882 val_alpha = %(val_alpha)s,
2883 val_unit = %(val_unit)s,
2884 val_normal_min = %(val_normal_min)s,
2885 val_normal_max = %(val_normal_max)s,
2886 val_normal_range = %(val_normal_range)s,
2887 val_target_min = %(val_target_min)s,
2888 val_target_max = %(val_target_max)s,
2889 val_target_range = %(val_target_range)s,
2890 abnormality_indicator = %(abnormal)s,
2891 norm_ref_group = %(ref_group)s,
2892 note_provider = %(note_provider)s,
2893 material = %(material)s,
2894 material_detail = %(material_detail)s
2895 where pk = %(pk_result)s""",
2896 """select xmin_test_result from v_results4lab_req where pk_result=%(pk_result)s"""
2897 ]
2898
2899 _updatable_fields = [
2900 'val_when',
2901 'progress_note_result',
2902 'val_num',
2903 'val_alpha',
2904 'val_unit',
2905 'val_normal_min',
2906 'val_normal_max',
2907 'val_normal_range',
2908 'val_target_min',
2909 'val_target_max',
2910 'val_target_range',
2911 'abnormal',
2912 'ref_group',
2913 'note_provider',
2914 'material',
2915 'material_detail'
2916 ]
2917
2918 - def __init__(self, aPK_obj=None, row=None):
2919 """Instantiate.
2920
2921 aPK_obj as dict:
2922 - patient_id
2923 - when_field (see view definition)
2924 - when
2925 - test_type
2926 - val_num
2927 - val_alpha
2928 - unit
2929 """
2930
2931 if aPK_obj is None:
2932 gmBusinessDBObject.cBusinessDBObject.__init__(self, row=row)
2933 return
2934 pk = aPK_obj
2935
2936 if type(aPK_obj) == dict:
2937
2938 if None in [aPK_obj['patient_id'], aPK_obj['when'], aPK_obj['when_field'], aPK_obj['test_type'], aPK_obj['unit']]:
2939 raise gmExceptions.ConstructorError('parameter error: %s' % aPK_obj)
2940 if (aPK_obj['val_num'] is None) and (aPK_obj['val_alpha'] is None):
2941 raise gmExceptions.ConstructorError('parameter error: val_num and val_alpha cannot both be None')
2942
2943 where_snippets = [
2944 'pk_patient=%(patient_id)s',
2945 'pk_test_type=%(test_type)s',
2946 '%s=%%(when)s' % aPK_obj['when_field'],
2947 'val_unit=%(unit)s'
2948 ]
2949 if aPK_obj['val_num'] is not None:
2950 where_snippets.append('val_num=%(val_num)s::numeric')
2951 if aPK_obj['val_alpha'] is not None:
2952 where_snippets.append('val_alpha=%(val_alpha)s')
2953
2954 where_clause = ' and '.join(where_snippets)
2955 cmd = "select pk_result from v_results4lab_req where %s" % where_clause
2956 data = gmPG.run_ro_query('historica', cmd, None, aPK_obj)
2957 if data is None:
2958 raise gmExceptions.ConstructorError('error getting lab result for: %s' % aPK_obj)
2959 if len(data) == 0:
2960 raise gmExceptions.NoSuchClinItemError('no lab result for: %s' % aPK_obj)
2961 pk = data[0][0]
2962
2963 gmBusinessDBObject.cBusinessDBObject.__init__(self, aPK_obj=pk)
2964
2966 cmd = """
2967 select
2968 %s,
2969 vbp.title,
2970 vbp.firstnames,
2971 vbp.lastnames,
2972 vbp.dob
2973 from v_active_persons vbp
2974 where vbp.pk_identity = %%s""" % self._payload[self._idx['pk_patient']]
2975 pat = gmPG.run_ro_query('historica', cmd, None, self._payload[self._idx['pk_patient']])
2976 return pat[0]
2977
2978
2979 -class cLabRequest(gmBusinessDBObject.cBusinessDBObject):
2980 """Represents one lab request."""
2981
2982 _cmd_fetch_payload = """
2983 select *, xmin_lab_request from v_lab_requests
2984 where pk_request=%s"""
2985 _cmds_lock_rows_for_update = [
2986 """select 1 from lab_request where pk=%(pk_request)s and xmin=%(xmin_lab_request)s for update"""
2987 ]
2988 _cmds_store_payload = [
2989 """update lab_request set
2990 request_id=%(request_id)s,
2991 lab_request_id=%(lab_request_id)s,
2992 clin_when=%(sampled_when)s,
2993 lab_rxd_when=%(lab_rxd_when)s,
2994 results_reported_when=%(results_reported_when)s,
2995 request_status=%(request_status)s,
2996 is_pending=%(is_pending)s::bool,
2997 narrative=%(progress_note)s
2998 where pk=%(pk_request)s""",
2999 """select xmin_lab_request from v_lab_requests where pk_request=%(pk_request)s"""
3000 ]
3001 _updatable_fields = [
3002 'request_id',
3003 'lab_request_id',
3004 'sampled_when',
3005 'lab_rxd_when',
3006 'results_reported_when',
3007 'request_status',
3008 'is_pending',
3009 'progress_note'
3010 ]
3011
3012 - def __init__(self, aPK_obj=None, row=None):
3013 """Instantiate lab request.
3014
3015 The aPK_obj can be either a dict with the keys "req_id"
3016 and "lab" or a simple primary key.
3017 """
3018
3019 if aPK_obj is None:
3020 gmBusinessDBObject.cBusinessDBObject.__init__(self, row=row)
3021 return
3022 pk = aPK_obj
3023
3024 if type(aPK_obj) == dict:
3025
3026 try:
3027 aPK_obj['req_id']
3028 aPK_obj['lab']
3029 except Exception:
3030 _log.exception('[%s:??]: faulty <aPK_obj> structure: [%s]' % (self.__class__.__name__, aPK_obj), sys.exc_info())
3031 raise gmExceptions.ConstructorError('[%s:??]: cannot derive PK from [%s]' % (self.__class__.__name__, aPK_obj))
3032
3033 where_snippets = []
3034 vals = {}
3035 where_snippets.append('request_id=%(req_id)s')
3036 if type(aPK_obj['lab']) == int:
3037 where_snippets.append('pk_test_org=%(lab)s')
3038 else:
3039 where_snippets.append('lab_name=%(lab)s')
3040 where_clause = ' and '.join(where_snippets)
3041 cmd = "select pk_request from v_lab_requests where %s" % where_clause
3042
3043 data = gmPG.run_ro_query('historica', cmd, None, aPK_obj)
3044 if data is None:
3045 raise gmExceptions.ConstructorError('[%s:??]: error getting lab request for [%s]' % (self.__class__.__name__, aPK_obj))
3046 if len(data) == 0:
3047 raise gmExceptions.NoSuchClinItemError('[%s:??]: no lab request for [%s]' % (self.__class__.__name__, aPK_obj))
3048 pk = data[0][0]
3049
3050 gmBusinessDBObject.cBusinessDBObject.__init__(self, aPK_obj=pk)
3051
3053 cmd = """
3054 select vpi.pk_patient, vbp.title, vbp.firstnames, vbp.lastnames, vbp.dob
3055 from v_pat_items vpi, v_active_persons vbp
3056 where
3057 vpi.pk_item=%s
3058 and
3059 vbp.pk_identity=vpi.pk_patient"""
3060 pat = gmPG.run_ro_query('historica', cmd, None, self._payload[self._idx['pk_item']])
3061 if pat is None:
3062 _log.error('cannot get patient for lab request [%s]' % self._payload[self._idx['pk_item']])
3063 return None
3064 if len(pat) == 0:
3065 _log.error('no patient associated with lab request [%s]' % self._payload[self._idx['pk_item']])
3066 return None
3067 return pat[0]
3068
3069
3070
3071
3072 -def create_lab_request(lab=None, req_id=None, pat_id=None, encounter_id=None, episode_id=None):
3073 """Create or get lab request.
3074
3075 returns tuple (status, value):
3076 (True, lab request instance)
3077 (False, error message)
3078 (None, housekeeping_todo primary key)
3079 """
3080 req = None
3081 aPK_obj = {
3082 'lab': lab,
3083 'req_id': req_id
3084 }
3085 try:
3086 req = cLabRequest (aPK_obj)
3087 except gmExceptions.NoSuchClinItemError as msg:
3088 _log.info('%s: will try to create lab request' % str(msg))
3089 except gmExceptions.ConstructorError as msg:
3090 _log.exception(str(msg), sys.exc_info(), verbose=0)
3091 return (False, msg)
3092
3093 if req is not None:
3094 db_pat = req.get_patient()
3095 if db_pat is None:
3096 _log.error('cannot cross-check patient on lab request')
3097 return (None, '')
3098
3099 if pat_id != db_pat[0]:
3100 _log.error('lab request found for [%s:%s] but patient mismatch: expected [%s], in DB [%s]' % (lab, req_id, pat_id, db_pat))
3101 me = '$RCSfile: gmPathLab.py,v $ $Revision: 1.81 $'
3102 to = 'user'
3103 prob = _('The lab request already exists but belongs to a different patient.')
3104 sol = _('Verify which patient this lab request really belongs to.')
3105 ctxt = _('lab [%s], request ID [%s], expected link with patient [%s], currently linked to patient [%s]') % (lab, req_id, pat_id, db_pat)
3106 cat = 'lab'
3107 status, data = gmPG.add_housekeeping_todo(me, to, prob, sol, ctxt, cat)
3108 return (None, data)
3109 return (True, req)
3110
3111 queries = []
3112 if type(lab) is int:
3113 cmd = "insert into lab_request (fk_encounter, fk_episode, fk_test_org, request_id) values (%s, %s, %s, %s)"
3114 else:
3115 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)"
3116 queries.append((cmd, [encounter_id, episode_id, str(lab), req_id]))
3117 cmd = "select currval('lab_request_pk_seq')"
3118 queries.append((cmd, []))
3119
3120 result, err = gmPG.run_commit('historica', queries, True)
3121 if result is None:
3122 return (False, err)
3123 try:
3124 req = cLabRequest(aPK_obj=result[0][0])
3125 except gmExceptions.ConstructorError as msg:
3126 _log.exception(str(msg), sys.exc_info(), verbose=0)
3127 return (False, msg)
3128 return (True, req)
3129
3130 -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):
3131 tres = None
3132 data = {
3133 'patient_id': patient_id,
3134 'when_field': when_field,
3135 'when': when,
3136 'test_type': test_type,
3137 'val_num': val_num,
3138 'val_alpha': val_alpha,
3139 'unit': unit
3140 }
3141 try:
3142 tres = cLabResult(aPK_obj=data)
3143
3144 _log.error('will not overwrite existing test result')
3145 _log.debug(str(tres))
3146 return (None, tres)
3147 except gmExceptions.NoSuchClinItemError:
3148 _log.debug('test result not found - as expected, will create it')
3149 except gmExceptions.ConstructorError as msg:
3150 _log.exception(str(msg), sys.exc_info(), verbose=0)
3151 return (False, msg)
3152 if request is None:
3153 return (False, _('need lab request when inserting lab result'))
3154
3155 if encounter_id is None:
3156 encounter_id = request['pk_encounter']
3157 queries = []
3158 cmd = "insert into test_result (fk_encounter, fk_episode, fk_type, val_num, val_alpha, val_unit) values (%s, %s, %s, %s, %s, %s)"
3159 queries.append((cmd, [encounter_id, request['pk_episode'], test_type, val_num, val_alpha, unit]))
3160 cmd = "insert into lnk_result2lab_req (fk_result, fk_request) values ((select currval('test_result_pk_seq')), %s)"
3161 queries.append((cmd, [request['pk_request']]))
3162 cmd = "select currval('test_result_pk_seq')"
3163 queries.append((cmd, []))
3164
3165 result, err = gmPG.run_commit('historica', queries, True)
3166 if result is None:
3167 return (False, err)
3168 try:
3169 tres = cLabResult(aPK_obj=result[0][0])
3170 except gmExceptions.ConstructorError as msg:
3171 _log.exception(str(msg), sys.exc_info(), verbose=0)
3172 return (False, msg)
3173 return (True, tres)
3174
3176
3177 if limit < 1:
3178 limit = 1
3179
3180 lim = limit + 1
3181 cmd = """
3182 select pk_result
3183 from v_results4lab_req
3184 where reviewed is false
3185 order by pk_patient
3186 limit %s""" % lim
3187 rows = gmPG.run_ro_query('historica', cmd)
3188 if rows is None:
3189 _log.error('error retrieving unreviewed lab results')
3190 return (None, _('error retrieving unreviewed lab results'))
3191 if len(rows) == 0:
3192 return (False, [])
3193
3194 if len(rows) == lim:
3195 more_avail = True
3196
3197 del rows[limit]
3198 else:
3199 more_avail = False
3200 results = []
3201 for row in rows:
3202 try:
3203 results.append(cLabResult(aPK_obj=row[0]))
3204 except gmExceptions.ConstructorError:
3205 _log.exception('skipping unreviewed lab result [%s]' % row[0], sys.exc_info(), verbose=0)
3206 return (more_avail, results)
3207
3208
3210 lim = limit + 1
3211 cmd = "select pk from lab_request where is_pending is true limit %s" % lim
3212 rows = gmPG.run_ro_query('historica', cmd)
3213 if rows is None:
3214 _log.error('error retrieving pending lab requests')
3215 return (None, None)
3216 if len(rows) == 0:
3217 return (False, [])
3218 results = []
3219
3220 if len(rows) == lim:
3221 too_many = True
3222
3223 del rows[limit]
3224 else:
3225 too_many = False
3226 requests = []
3227 for row in rows:
3228 try:
3229 requests.append(cLabRequest(aPK_obj=row[0]))
3230 except gmExceptions.ConstructorError:
3231 _log.exception('skipping pending lab request [%s]' % row[0], sys.exc_info(), verbose=0)
3232 return (too_many, requests)
3233
3234
3236 """Get logically next request ID for given lab.
3237
3238 - incrementor_func:
3239 - if not supplied the next ID is guessed
3240 - if supplied it is applied to the most recently used ID
3241 """
3242 if type(lab) == int:
3243 lab_snippet = 'vlr.fk_test_org=%s'
3244 else:
3245 lab_snippet = 'vlr.lab_name=%s'
3246 lab = str(lab)
3247 cmd = """
3248 select request_id
3249 from lab_request lr0
3250 where lr0.clin_when = (
3251 select max(vlr.sampled_when)
3252 from v_lab_requests vlr
3253 where %s
3254 )""" % lab_snippet
3255 rows = gmPG.run_ro_query('historica', cmd, None, lab)
3256 if rows is None:
3257 _log.warning('error getting most recently used request ID for lab [%s]' % lab)
3258 return ''
3259 if len(rows) == 0:
3260 return ''
3261 most_recent = rows[0][0]
3262
3263 if incrementor_func is not None:
3264 try:
3265 next = incrementor_func(most_recent)
3266 except TypeError:
3267 _log.error('cannot call incrementor function [%s]' % str(incrementor_func))
3268 return most_recent
3269 return next
3270
3271 for pos in range(len(most_recent)):
3272 header = most_recent[:pos]
3273 trailer = most_recent[pos:]
3274 try:
3275 return '%s%s' % (header, str(int(trailer) + 1))
3276 except ValueError:
3277 header = most_recent[:-1]
3278 trailer = most_recent[-1:]
3279 return '%s%s' % (header, chr(ord(trailer) + 1))
3280
3281
3283 """Calculate BMI.
3284
3285 mass: kg
3286 height: cm
3287 age: not yet used
3288
3289 returns:
3290 (True/False, data)
3291 True: data = (bmi, lower_normal, upper_normal)
3292 False: data = error message
3293 """
3294 converted, mass = gmTools.input2decimal(mass)
3295 if not converted:
3296 return False, 'mass: cannot convert <%s> to Decimal' % mass
3297
3298 converted, height = gmTools.input2decimal(height)
3299 if not converted:
3300 return False, 'height: cannot convert <%s> to Decimal' % height
3301
3302 approx_surface = (height / decimal.Decimal(100))**2
3303 bmi = mass / approx_surface
3304
3305 print(mass, height, '->', approx_surface, '->', bmi)
3306
3307 lower_normal_mass = 20.0 * approx_surface
3308 upper_normal_mass = 25.0 * approx_surface
3309
3310 return True, (bmi, lower_normal_mass, upper_normal_mass)
3311
3312
3313
3314
3315 if __name__ == '__main__':
3316
3317 if len(sys.argv) < 2:
3318 sys.exit()
3319
3320 if sys.argv[1] != 'test':
3321 sys.exit()
3322
3323 import time
3324
3325 gmI18N.activate_locale()
3326 gmI18N.install_domain()
3327
3328
3330 tr = create_test_result (
3331 encounter = 1,
3332 episode = 1,
3333 type = 1,
3334 intended_reviewer = 1,
3335 val_num = '12',
3336 val_alpha=None,
3337 unit = 'mg/dl'
3338 )
3339 print(tr)
3340 return tr
3341
3345
3353
3355 print("test_result()")
3356
3357 data = {
3358 'patient_id': 12,
3359 'when_field': 'val_when',
3360 'when': '2000-09-17 18:23:00+02',
3361 'test_type': 9,
3362 'val_num': 17.3,
3363 'val_alpha': None,
3364 'unit': 'mg/l'
3365 }
3366 lab_result = cLabResult(aPK_obj=data)
3367 print(lab_result)
3368 fields = lab_result.get_fields()
3369 for field in fields:
3370 print(field, ':', lab_result[field])
3371 print("updatable:", lab_result.get_updatable_fields())
3372 print(time.time())
3373 print(lab_result.get_patient())
3374 print(time.time())
3375
3377 print("test_request()")
3378 try:
3379
3380
3381 data = {
3382 'req_id': 'EML#SC937-0176-CEC#11',
3383 'lab': 'Enterprise Main Lab'
3384 }
3385 lab_req = cLabRequest(aPK_obj=data)
3386 except gmExceptions.ConstructorError as msg:
3387 print("no such lab request:", msg)
3388 return
3389 print(lab_req)
3390 fields = lab_req.get_fields()
3391 for field in fields:
3392 print(field, ':', lab_req[field])
3393 print("updatable:", lab_req.get_updatable_fields())
3394 print(time.time())
3395 print(lab_req.get_patient())
3396 print(time.time())
3397
3402
3407
3415
3420
3425
3434
3436 done, data = calculate_bmi(mass = sys.argv[2], height = sys.argv[3])
3437 bmi, low, high = data
3438 print("BMI:", bmi)
3439 print("low:", low, "kg")
3440 print("hi :", high, "kg")
3441
3442
3448
3449
3451 tp = cTestPanel(aPK_obj = 1)
3452
3453
3454 print(tp.format())
3455
3456
3457
3458 most_recent = tp.get_most_recent_results(pk_patient = 12, group_by_meta_type = True, include_missing = True)
3459
3460 print('found:', len(most_recent))
3461
3462 for t in most_recent:
3463 print('--------------')
3464 if t['pk_meta_test_type'] is None:
3465 print("standalone")
3466 else:
3467 print("meta")
3468 print(t.format())
3469
3470
3472 most_recent = get_most_recent_results_in_loinc_group (
3473
3474
3475 loincs = ['2160-0', '14682-9', '40264-4', '40248-7'],
3476 max_no_of_results = 6,
3477 patient = 201,
3478 consider_indirect_matches = False
3479 )
3480 for t in most_recent:
3481 print(t['loinc_tt'], t['loinc_meta'], t['unified_loinc'])
3482 if t['pk_meta_test_type'] is None:
3483 print("---- standalone ----")
3484 else:
3485 print("---- meta ----")
3486 print(t.format())
3487 input('next')
3488
3489 return
3490
3491 most_recent = get_most_recent_results_in_loinc_group (
3492
3493
3494 loincs = ['2160-0', '14682-9', '40264-4', '40248-7'],
3495 max_no_of_results = 2,
3496 patient = 201,
3497 consider_indirect_matches = False
3498
3499 )
3500 for t in most_recent:
3501 if t['pk_meta_test_type'] is None:
3502 print("---- standalone ----")
3503 else:
3504 print("---- meta ----")
3505 print(t.format())
3506
3507
3508
3509
3510
3511
3512
3513
3514
3515
3516
3517
3518
3519
3520
3521
3522
3523
3524 test_get_most_recent_results_in_loinc_group()
3525
3526
3527