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
15
16 if __name__ == '__main__':
17 sys.path.insert(0, '../../')
18
19 from Gnumed.pycommon import gmDateTime
20 if __name__ == '__main__':
21 from Gnumed.pycommon import gmLog2
22 from Gnumed.pycommon import gmI18N
23 gmI18N.activate_locale()
24 gmI18N.install_domain('gnumed')
25 gmDateTime.init()
26 from Gnumed.pycommon import gmExceptions
27 from Gnumed.pycommon import gmBusinessDBObject
28 from Gnumed.pycommon import gmPG2
29 from Gnumed.pycommon import gmTools
30 from Gnumed.pycommon import gmDispatcher
31 from Gnumed.pycommon import gmHooks
32
33 from Gnumed.business import gmOrganization
34 from Gnumed.business import gmCoding
35
36 _log = logging.getLogger('gm.lab')
37
38
39 HL7_RESULT_STATI = {
40 None: _('unknown'),
41 '': _('empty status'),
42 'C': _('C (HL7: Correction, replaces previous final)'),
43 'D': _('D (HL7: Deletion)'),
44 'F': _('F (HL7: Final)'),
45 'I': _('I (HL7: pending, specimen In lab)'),
46 'P': _('P (HL7: Preliminary)'),
47 'R': _('R (HL7: result entered, not yet verified)'),
48 'S': _('S (HL7: partial)'),
49 'X': _('X (HL7: cannot obtain results for this observation)'),
50 'U': _('U (HL7: mark as final (I/P/R/S -> F, value Unchanged)'),
51 'W': _('W (HL7: original Wrong (say, wrong patient))')
52 }
53
54 URL_test_result_information = 'http://www.laborlexikon.de'
55 URL_test_result_information_search = "http://www.google.de/search?as_oq=%(search_term)s&num=10&as_sitesearch=laborlexikon.de"
56
57
61
62 gmDispatcher.connect(_on_test_result_modified, 'clin.test_result_mod_db')
63
64
65 _SQL_get_test_orgs = "SELECT * FROM clin.v_test_orgs WHERE %s"
66
67 -class cTestOrg(gmBusinessDBObject.cBusinessDBObject):
68 """Represents one test org/lab."""
69 _cmd_fetch_payload = _SQL_get_test_orgs % 'pk_test_org = %s'
70 _cmds_store_payload = [
71 """UPDATE clin.test_org SET
72 fk_org_unit = %(pk_org_unit)s,
73 contact = gm.nullify_empty_string(%(test_org_contact)s),
74 comment = gm.nullify_empty_string(%(comment)s)
75 WHERE
76 pk = %(pk_test_org)s
77 AND
78 xmin = %(xmin_test_org)s
79 RETURNING
80 xmin AS xmin_test_org
81 """
82 ]
83 _updatable_fields = [
84 'pk_org_unit',
85 'test_org_contact',
86 'comment'
87 ]
88
89 -def create_test_org(name=None, comment=None, pk_org_unit=None, link_obj=None):
90
91 _log.debug('creating test org [%s:%s:%s]', name, comment, pk_org_unit)
92
93 if name is None:
94 name = 'unassigned lab'
95
96
97 if pk_org_unit is None:
98 org = gmOrganization.create_org (
99 link_obj = link_obj,
100 organization = name,
101 category = 'Laboratory'
102 )
103 org_unit = gmOrganization.create_org_unit (
104 link_obj = link_obj,
105 pk_organization = org['pk_org'],
106 unit = name
107 )
108 pk_org_unit = org_unit['pk_org_unit']
109
110
111 args = {'pk_unit': pk_org_unit}
112 cmd = 'SELECT pk_test_org FROM clin.v_test_orgs WHERE pk_org_unit = %(pk_unit)s'
113 rows, idx = gmPG2.run_ro_queries(link_obj = link_obj, queries = [{'cmd': cmd, 'args': args}])
114
115 if len(rows) == 0:
116 cmd = 'INSERT INTO clin.test_org (fk_org_unit) VALUES (%(pk_unit)s) RETURNING pk'
117 rows, idx = gmPG2.run_rw_queries(link_obj = link_obj, queries = [{'cmd': cmd, 'args': args}], return_data = True)
118
119 test_org = cTestOrg(link_obj = link_obj, aPK_obj = rows[0][0])
120 if comment is not None:
121 comment = comment.strip()
122 test_org['comment'] = comment
123 test_org.save(conn = link_obj)
124
125 return test_org
126
128 args = {'pk': test_org}
129 cmd = """
130 DELETE FROM clin.test_org
131 WHERE
132 pk = %(pk)s
133 AND
134 NOT EXISTS (SELECT 1 FROM clin.lab_request WHERE fk_test_org = %(pk)s LIMIT 1)
135 AND
136 NOT EXISTS (SELECT 1 FROM clin.test_type WHERE fk_test_org = %(pk)s LIMIT 1)
137 """
138 gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
139
141 cmd = 'SELECT * FROM clin.v_test_orgs ORDER BY %s' % order_by
142 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd}], get_col_idx = True)
143 if return_pks:
144 return [ r['pk_test_org'] for r in rows ]
145 return [ cTestOrg(row = {'pk_field': 'pk_test_org', 'data': r, 'idx': idx}) for r in rows ]
146
147
148
149
150 _SQL_get_test_panels = "SELECT * FROM clin.v_test_panels WHERE %s"
151
152 -class cTestPanel(gmBusinessDBObject.cBusinessDBObject):
153 """Represents a grouping/listing of tests into a panel."""
154
155 _cmd_fetch_payload = _SQL_get_test_panels % "pk_test_panel = %s"
156 _cmds_store_payload = [
157 """
158 UPDATE clin.test_panel SET
159 description = gm.nullify_empty_string(%(description)s),
160 comment = gm.nullify_empty_string(%(comment)s)
161 WHERE
162 pk = %(pk_test_panel)s
163 AND
164 xmin = %(xmin_test_panel)s
165 RETURNING
166 xmin AS xmin_test_panel
167 """
168 ]
169 _updatable_fields = [
170 'description',
171 'comment'
172 ]
173
220
221
223 """<pk_code> must be a value from ref.coding_system_root.pk_coding_system (clin.lnk_code2item_root.fk_generic_code)"""
224 cmd = "INSERT INTO clin.lnk_code2tst_pnl (fk_item, fk_generic_code) values (%(tp)s, %(code)s)"
225 args = {
226 'tp': self._payload[self._idx['pk_test_panel']],
227 'code': pk_code
228 }
229 rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
230 return True
231
232
234 """<pk_code> must be a value from ref.coding_system_root.pk_coding_system (clin.lnk_code2item_root.fk_generic_code)"""
235 cmd = "DELETE FROM clin.lnk_code2tst_pnl WHERE fk_item = %(tp)s AND fk_generic_code = %(code)s"
236 args = {
237 'tp': self._payload[self._idx['pk_test_panel']],
238 'code': pk_code
239 }
240 rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
241 return True
242
243
245 """Retrieve data about test types on this panel (for which this patient has results)."""
246
247 if order_by is None:
248 order_by = ''
249 else:
250 order_by = 'ORDER BY %s' % order_by
251
252 if unique_meta_types:
253 cmd = """
254 SELECT * FROM clin.v_test_types c_vtt
255 WHERE c_vtt.pk_test_type IN (
256 SELECT DISTINCT ON (c_vtr1.pk_meta_test_type) c_vtr1.pk_test_type
257 FROM clin.v_test_results c_vtr1
258 WHERE
259 c_vtr1.pk_test_type IN %%(pks)s
260 AND
261 c_vtr1.pk_patient = %%(pat)s
262 AND
263 c_vtr1.pk_meta_test_type IS NOT NULL
264 UNION ALL
265 SELECT DISTINCT ON (c_vtr2.pk_test_type) c_vtr2.pk_test_type
266 FROM clin.v_test_results c_vtr2
267 WHERE
268 c_vtr2.pk_test_type IN %%(pks)s
269 AND
270 c_vtr2.pk_patient = %%(pat)s
271 AND
272 c_vtr2.pk_meta_test_type IS NULL
273 )
274 %s""" % order_by
275 else:
276 cmd = """
277 SELECT * FROM clin.v_test_types c_vtt
278 WHERE c_vtt.pk_test_type IN (
279 SELECT DISTINCT ON (c_vtr.pk_test_type) c_vtr.pk_test_type
280 FROM clin.v_test_results c_vtr
281 WHERE
282 c_vtr.pk_test_type IN %%(pks)s
283 AND
284 c_vtr.pk_patient = %%(pat)s
285 )
286 %s""" % order_by
287
288 args = {
289 'pat': pk_patient,
290 'pks': tuple([ tt['pk_test_type'] for tt in self._payload[self._idx['test_types']] ])
291 }
292 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
293 return [ cMeasurementType(row = {'pk_field': 'pk_test_type', 'idx': idx, 'data': r}) for r in rows ]
294
295
297 if self._payload[self._idx['loincs']] is not None:
298 if loinc in self._payload[self._idx['loincs']]:
299 return
300 gmPG2.run_rw_queries(queries = [{
301 'cmd': 'INSERT INTO clin.lnk_loinc2test_panel (fk_test_panel, loinc) VALUES (%(pk_pnl)s, %(loinc)s)',
302 'args': {'loinc': loinc, 'pk_pnl': self._payload[self._idx['pk_test_panel']]}
303 }])
304 return
305
306
308 if self._payload[self._idx['loincs']] is None:
309 return
310 if loinc not in self._payload[self._idx['loincs']]:
311 return
312 gmPG2.run_rw_queries(queries = [{
313 'cmd': 'DELETE FROM clin.lnk_loinc2test_panel WHERE fk_test_panel = %(pk_pnl)s AND loinc = %(loinc)s',
314 'args': {'loinc': loinc, 'pk_pnl': self._payload[self._idx['pk_test_panel']]}
315 }])
316 return
317
318
319
320
322 return self._payload[self._idx['loincs']]
323
325 queries = []
326
327 if len(loincs) == 0:
328 cmd = 'DELETE FROM clin.lnk_loinc2test_panel WHERE fk_test_panel = %(pk_pnl)s'
329 else:
330 cmd = 'DELETE FROM clin.lnk_loinc2test_panel WHERE fk_test_panel = %(pk_pnl)s AND loinc NOT IN %(loincs)s'
331 queries.append({'cmd': cmd, 'args': {'loincs': tuple(loincs), 'pk_pnl': self._payload[self._idx['pk_test_panel']]}})
332
333 if len(loincs) > 0:
334 for loinc in loincs:
335 cmd = """INSERT INTO clin.lnk_loinc2test_panel (fk_test_panel, loinc)
336 SELECT %(pk_pnl)s, %(loinc)s WHERE NOT EXISTS (
337 SELECT 1 FROM clin.lnk_loinc2test_panel WHERE
338 fk_test_panel = %(pk_pnl)s
339 AND
340 loinc = %(loinc)s
341 )"""
342 queries.append({'cmd': cmd, 'args': {'loinc': loinc, 'pk_pnl': self._payload[self._idx['pk_test_panel']]}})
343 return gmPG2.run_rw_queries(queries = queries)
344
345 included_loincs = property(_get_included_loincs, _set_included_loincs)
346
347
349 if len(self._payload[self._idx['test_types']]) == 0:
350 return []
351
352 rows, idx = gmPG2.run_ro_queries (
353 queries = [{
354 'cmd': _SQL_get_test_types % 'pk_test_type IN %(pks)s ORDER BY unified_abbrev',
355 'args': {'pks': tuple([ tt['pk_test_type'] for tt in self._payload[self._idx['test_types']] ])}
356 }],
357 get_col_idx = True
358 )
359 return [ cMeasurementType(row = {'data': r, 'idx': idx, 'pk_field': 'pk_test_type'}) for r in rows ]
360
361 test_types = property(_get_test_types, lambda x:x)
362
363
365 if len(self._payload[self._idx['pk_generic_codes']]) == 0:
366 return []
367
368 cmd = gmCoding._SQL_get_generic_linked_codes % 'pk_generic_code IN %(pks)s'
369 args = {'pks': tuple(self._payload[self._idx['pk_generic_codes']])}
370 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
371 return [ gmCoding.cGenericLinkedCode(row = {'data': r, 'idx': idx, 'pk_field': 'pk_lnk_code2item'}) for r in rows ]
372
374 queries = []
375
376 if len(self._payload[self._idx['pk_generic_codes']]) > 0:
377 queries.append ({
378 'cmd': 'DELETE FROM clin.lnk_code2tst_pnl WHERE fk_item = %(tp)s AND fk_generic_code IN %(codes)s',
379 'args': {
380 'tp': self._payload[self._idx['pk_test_panel']],
381 'codes': tuple(self._payload[self._idx['pk_generic_codes']])
382 }
383 })
384
385 for pk_code in pk_codes:
386 queries.append ({
387 'cmd': 'INSERT INTO clin.lnk_code2test_panel (fk_item, fk_generic_code) VALUES (%(tp)s, %(pk_code)s)',
388 'args': {
389 'tp': self._payload[self._idx['pk_test_panel']],
390 'pk_code': pk_code
391 }
392 })
393 if len(queries) == 0:
394 return
395
396 rows, idx = gmPG2.run_rw_queries(queries = queries)
397 return
398
399 generic_codes = property(_get_generic_codes, _set_generic_codes)
400
401
402 - def get_most_recent_results(self, pk_patient=None, order_by=None, group_by_meta_type=False, include_missing=False):
403
404 if len(self._payload[self._idx['test_types']]) == 0:
405 return []
406
407 pnl_results = get_most_recent_results_for_panel (
408 pk_patient = pk_patient,
409 pk_panel = self._payload[self._idx['pk_test_panel']],
410 order_by = order_by,
411 group_by_meta_type = group_by_meta_type
412 )
413 if not include_missing:
414 return pnl_results
415
416 loincs_found = [ r['loinc_tt'] for r in pnl_results ]
417 loincs_found.extend([ r['loinc_meta'] for r in pnl_results if r['loinc_meta'] not in loincs_found ])
418 loincs2consider = set([ tt['loinc'] for tt in self._payload[self._idx['test_types']] ])
419 loincs_missing = loincs2consider - set(loincs_found)
420 pnl_results.extend(loincs_missing)
421 return pnl_results
422
423
425 where_args = {}
426 if loincs is None:
427 where_parts = ['true']
428 else:
429 where_parts = ['loincs @> %(loincs)s']
430 where_args['loincs'] = list(loincs)
431
432 if order_by is None:
433 order_by = u''
434 else:
435 order_by = ' ORDER BY %s' % order_by
436
437 cmd = (_SQL_get_test_panels % ' AND '.join(where_parts)) + order_by
438 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': where_args}], get_col_idx = True)
439 if return_pks:
440 return [ r['pk_test_panel'] for r in rows ]
441 return [ cTestPanel(row = {'data': r, 'idx': idx, 'pk_field': 'pk_test_panel'}) for r in rows ]
442
443
445
446 args = {'desc': description.strip()}
447 cmd = """
448 INSERT INTO clin.test_panel (description)
449 VALUES (gm.nullify_empty_string(%(desc)s))
450 RETURNING pk
451 """
452 rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}], return_data = True, get_col_idx = False)
453
454 return cTestPanel(aPK_obj = rows[0]['pk'])
455
456
458 args = {'pk': pk}
459 cmd = "DELETE FROM clin.test_panel WHERE pk = %(pk)s"
460 gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
461 return True
462
463
629
630
658
659
672
673
680
681
682 _SQL_get_test_types = "SELECT * FROM clin.v_test_types WHERE %s"
683
685 """Represents one test result type."""
686
687 _cmd_fetch_payload = _SQL_get_test_types % "pk_test_type = %s"
688
689 _cmds_store_payload = [
690 """UPDATE clin.test_type SET
691 abbrev = gm.nullify_empty_string(%(abbrev)s),
692 name = gm.nullify_empty_string(%(name)s),
693 loinc = gm.nullify_empty_string(%(loinc)s),
694 comment = gm.nullify_empty_string(%(comment_type)s),
695 reference_unit = gm.nullify_empty_string(%(reference_unit)s),
696 fk_test_org = %(pk_test_org)s,
697 fk_meta_test_type = %(pk_meta_test_type)s
698 WHERE
699 pk = %(pk_test_type)s
700 AND
701 xmin = %(xmin_test_type)s
702 RETURNING
703 xmin AS xmin_test_type"""
704 ]
705
706 _updatable_fields = [
707 'abbrev',
708 'name',
709 'loinc',
710 'comment_type',
711 'reference_unit',
712 'pk_test_org',
713 'pk_meta_test_type'
714 ]
715
716
717
719 cmd = 'SELECT EXISTS(SELECT 1 FROM clin.test_result WHERE fk_type = %(pk_type)s)'
720 args = {'pk_type': self._payload[self._idx['pk_test_type']]}
721 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}])
722 return rows[0][0]
723
724 in_use = property(_get_in_use, lambda x:x)
725
726
747
748
763
764
766 if self._payload[self._idx['pk_test_panels']] is None:
767 return None
768
769 return [ cTestPanel(aPK_obj = pk) for pk in self._payload[self._idx['pk_test_panels']] ]
770
771 test_panels = property(_get_test_panels, lambda x:x)
772
773
780
781 meta_test_type = property(get_meta_test_type, lambda x:x)
782
783
785 """Returns the closest test result which does have normal range information.
786
787 - needs <unit>
788 - if <timestamp> is None it will assume now() and thus return the most recent
789 """
790 if timestamp is None:
791 timestamp = gmDateTime.pydt_now_here()
792 cmd = """
793 SELECT * FROM clin.v_test_results
794 WHERE
795 pk_test_type = %(pk_type)s
796 AND
797 val_unit = %(unit)s
798 AND
799 (
800 (val_normal_min IS NOT NULL)
801 OR
802 (val_normal_max IS NOT NULL)
803 OR
804 (val_normal_range IS NOT NULL)
805 )
806 ORDER BY
807 CASE
808 WHEN clin_when > %(clin_when)s THEN clin_when - %(clin_when)s
809 ELSE %(clin_when)s - clin_when
810 END
811 LIMIT 1"""
812 args = {
813 'pk_type': self._payload[self._idx['pk_test_type']],
814 'unit': unit,
815 'clin_when': timestamp
816 }
817 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
818 if len(rows) == 0:
819 return None
820 r = rows[0]
821 return cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': r})
822
823
825 """Returns the closest test result which does have target range information.
826
827 - needs <unit>
828 - needs <patient> (as target will be per-patient)
829 - if <timestamp> is None it will assume now() and thus return the most recent
830 """
831 if timestamp is None:
832 timestamp = gmDateTime.pydt_now_here()
833 cmd = """
834 SELECT * FROM clin.v_test_results
835 WHERE
836 pk_test_type = %(pk_type)s
837 AND
838 val_unit = %(unit)s
839 AND
840 pk_patient = %(pat)s
841 AND
842 (
843 (val_target_min IS NOT NULL)
844 OR
845 (val_target_max IS NOT NULL)
846 OR
847 (val_target_range IS NOT NULL)
848 )
849 ORDER BY
850 CASE
851 WHEN clin_when > %(clin_when)s THEN clin_when - %(clin_when)s
852 ELSE %(clin_when)s - clin_when
853 END
854 LIMIT 1"""
855 args = {
856 'pk_type': self._payload[self._idx['pk_test_type']],
857 'unit': unit,
858 'pat': patient,
859 'clin_when': timestamp
860 }
861 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
862 if len(rows) == 0:
863 return None
864 r = rows[0]
865 return cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': r})
866
867
869 """Returns the unit of the closest test result.
870
871 - if <timestamp> is None it will assume now() and thus return the most recent
872 """
873 if timestamp is None:
874 timestamp = gmDateTime.pydt_now_here()
875 cmd = """
876 SELECT val_unit FROM clin.v_test_results
877 WHERE
878 pk_test_type = %(pk_type)s
879 AND
880 val_unit IS NOT NULL
881 ORDER BY
882 CASE
883 WHEN clin_when > %(clin_when)s THEN clin_when - %(clin_when)s
884 ELSE %(clin_when)s - clin_when
885 END
886 LIMIT 1"""
887 args = {
888 'pk_type': self._payload[self._idx['pk_test_type']],
889 'clin_when': timestamp
890 }
891 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
892 if len(rows) == 0:
893 return None
894 return rows[0]['val_unit']
895
896 temporally_closest_unit = property(get_temporally_closest_unit, lambda x:x)
897
898
959
960
962 args = {}
963 where_parts = []
964 if loincs is not None:
965 if len(loincs) > 0:
966 where_parts.append('loinc IN %(loincs)s')
967 args['loincs'] = tuple(loincs)
968 if len(where_parts) == 0:
969 where_parts.append('TRUE')
970 WHERE_clause = ' AND '.join(where_parts)
971 cmd = (_SQL_get_test_types % WHERE_clause) + gmTools.coalesce(order_by, '', ' ORDER BY %s')
972
973 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
974 if return_pks:
975 return [ r['pk_test_type'] for r in rows ]
976 return [ cMeasurementType(row = {'pk_field': 'pk_test_type', 'data': r, 'idx': idx}) for r in rows ]
977
978
980
981 if (abbrev is None) and (name is None):
982 raise ValueError('must have <abbrev> and/or <name> set')
983
984 where_snippets = []
985
986 if lab is None:
987 where_snippets.append('pk_test_org IS NULL')
988 else:
989 try:
990 int(lab)
991 where_snippets.append('pk_test_org = %(lab)s')
992 except (TypeError, ValueError):
993 where_snippets.append('pk_test_org = (SELECT pk_test_org FROM clin.v_test_orgs WHERE unit = %(lab)s)')
994
995 if abbrev is not None:
996 where_snippets.append('abbrev = %(abbrev)s')
997
998 if name is not None:
999 where_snippets.append('name = %(name)s')
1000
1001 where_clause = ' and '.join(where_snippets)
1002 cmd = "select * from clin.v_test_types where %s" % where_clause
1003 args = {'lab': lab, 'abbrev': abbrev, 'name': name}
1004
1005 rows, idx = gmPG2.run_ro_queries(link_obj = link_obj, queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
1006
1007 if len(rows) == 0:
1008 return None
1009
1010 tt = cMeasurementType(row = {'pk_field': 'pk_test_type', 'data': rows[0], 'idx': idx})
1011 return tt
1012
1013
1015 cmd = 'delete from clin.test_type where pk = %(pk)s'
1016 args = {'pk': measurement_type}
1017 gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
1018
1019
1021 """Create or get test type."""
1022
1023 ttype = find_measurement_type(lab = lab, abbrev = abbrev, name = name, link_obj = link_obj)
1024
1025 if ttype is not None:
1026 return ttype
1027
1028 _log.debug('creating test type [%s:%s:%s:%s]', lab, abbrev, name, unit)
1029
1030
1031
1032
1033
1034
1035
1036 cols = []
1037 val_snippets = []
1038 vals = {}
1039
1040
1041 if lab is None:
1042 lab = create_test_org(link_obj = link_obj)['pk_test_org']
1043
1044 cols.append('fk_test_org')
1045 try:
1046 vals['lab'] = int(lab)
1047 val_snippets.append('%(lab)s')
1048 except:
1049 vals['lab'] = lab
1050 val_snippets.append('(SELECT pk_test_org FROM clin.v_test_orgs WHERE unit = %(lab)s)')
1051
1052
1053 cols.append('abbrev')
1054 val_snippets.append('%(abbrev)s')
1055 vals['abbrev'] = abbrev
1056
1057
1058 if unit is not None:
1059 cols.append('reference_unit')
1060 val_snippets.append('%(unit)s')
1061 vals['unit'] = unit
1062
1063
1064 if name is not None:
1065 cols.append('name')
1066 val_snippets.append('%(name)s')
1067 vals['name'] = name
1068
1069 col_clause = ', '.join(cols)
1070 val_clause = ', '.join(val_snippets)
1071 queries = [
1072 {'cmd': 'insert into clin.test_type (%s) values (%s)' % (col_clause, val_clause), 'args': vals},
1073 {'cmd': "select * from clin.v_test_types where pk_test_type = currval(pg_get_serial_sequence('clin.test_type', 'pk'))"}
1074 ]
1075 rows, idx = gmPG2.run_rw_queries(link_obj = link_obj, queries = queries, get_col_idx = True, return_data = True)
1076 ttype = cMeasurementType(row = {'pk_field': 'pk_test_type', 'data': rows[0], 'idx': idx})
1077
1078 return ttype
1079
1080
1081 -class cTestResult(gmBusinessDBObject.cBusinessDBObject):
1082 """Represents one test result."""
1083
1084 _cmd_fetch_payload = "select * from clin.v_test_results where pk_test_result = %s"
1085
1086 _cmds_store_payload = [
1087 """UPDATE clin.test_result SET
1088 clin_when = %(clin_when)s,
1089 narrative = nullif(trim(%(comment)s), ''),
1090 val_num = %(val_num)s,
1091 val_alpha = nullif(trim(%(val_alpha)s), ''),
1092 val_unit = nullif(trim(%(val_unit)s), ''),
1093 val_normal_min = %(val_normal_min)s,
1094 val_normal_max = %(val_normal_max)s,
1095 val_normal_range = nullif(trim(%(val_normal_range)s), ''),
1096 val_target_min = %(val_target_min)s,
1097 val_target_max = %(val_target_max)s,
1098 val_target_range = nullif(trim(%(val_target_range)s), ''),
1099 abnormality_indicator = nullif(trim(%(abnormality_indicator)s), ''),
1100 norm_ref_group = nullif(trim(%(norm_ref_group)s), ''),
1101 note_test_org = nullif(trim(%(note_test_org)s), ''),
1102 material = nullif(trim(%(material)s), ''),
1103 material_detail = nullif(trim(%(material_detail)s), ''),
1104 status = gm.nullify_empty_string(%(status)s),
1105 val_grouping = gm.nullify_empty_string(%(val_grouping)s),
1106 source_data = gm.nullify_empty_string(%(source_data)s),
1107 fk_intended_reviewer = %(pk_intended_reviewer)s,
1108 fk_encounter = %(pk_encounter)s,
1109 fk_episode = %(pk_episode)s,
1110 fk_type = %(pk_test_type)s,
1111 fk_request = %(pk_request)s
1112 WHERE
1113 pk = %(pk_test_result)s AND
1114 xmin = %(xmin_test_result)s
1115 RETURNING
1116 xmin AS xmin_test_result
1117 """
1118
1119 ]
1120
1121 _updatable_fields = [
1122 'clin_when',
1123 'comment',
1124 'val_num',
1125 'val_alpha',
1126 'val_unit',
1127 'val_normal_min',
1128 'val_normal_max',
1129 'val_normal_range',
1130 'val_target_min',
1131 'val_target_max',
1132 'val_target_range',
1133 'abnormality_indicator',
1134 'norm_ref_group',
1135 'note_test_org',
1136 'material',
1137 'material_detail',
1138 'status',
1139 'val_grouping',
1140 'source_data',
1141 'pk_intended_reviewer',
1142 'pk_encounter',
1143 'pk_episode',
1144 'pk_test_type',
1145 'pk_request'
1146 ]
1147
1148
1181
1182
1430
1431
1433 return (
1434 self._payload[self._idx['val_normal_min']] is not None
1435 ) or (
1436 self._payload[self._idx['val_normal_max']] is not None
1437 )
1438
1439 has_normal_min_or_max = property(_get_has_normal_min_or_max, lambda x:x)
1440
1441
1443 has_range_info = (
1444 self._payload[self._idx['val_normal_min']] is not None
1445 ) or (
1446 self._payload[self._idx['val_normal_max']] is not None
1447 )
1448 if has_range_info is False:
1449 return None
1450
1451 return '%s - %s' % (
1452 gmTools.coalesce(self._payload[self._idx['val_normal_min']], '?'),
1453 gmTools.coalesce(self._payload[self._idx['val_normal_max']], '?')
1454 )
1455
1456 normal_min_max = property(_get_normal_min_max, lambda x:x)
1457
1458
1485
1486 formatted_normal_range = property(_get_formatted_normal_range, lambda x:x)
1487
1488
1490 return (
1491 self._payload[self._idx['val_target_min']] is not None
1492 ) or (
1493 self._payload[self._idx['val_target_max']] is not None
1494 )
1495
1496 has_clinical_min_or_max = property(_get_has_clinical_min_or_max, lambda x:x)
1497
1498
1500 has_range_info = (
1501 self._payload[self._idx['val_target_min']] is not None
1502 ) or (
1503 self._payload[self._idx['val_target_max']] is not None
1504 )
1505 if has_range_info is False:
1506 return None
1507
1508 return '%s - %s' % (
1509 gmTools.coalesce(self._payload[self._idx['val_target_min']], '?'),
1510 gmTools.coalesce(self._payload[self._idx['val_target_max']], '?')
1511 )
1512
1513 clinical_min_max = property(_get_clinical_min_max, lambda x:x)
1514
1515
1542
1543 formatted_clinical_range = property(_get_formatted_clinical_range, lambda x:x)
1544
1545
1547 """Returns the closest test result which does have normal range information."""
1548 if self._payload[self._idx['val_normal_min']] is not None:
1549 return self
1550 if self._payload[self._idx['val_normal_max']] is not None:
1551 return self
1552 if self._payload[self._idx['val_normal_range']] is not None:
1553 return self
1554 cmd = """
1555 SELECT * from clin.v_test_results
1556 WHERE
1557 pk_type = %(pk_type)s
1558 AND
1559 val_unit = %(unit)s
1560 AND
1561 (
1562 (val_normal_min IS NOT NULL)
1563 OR
1564 (val_normal_max IS NOT NULL)
1565 OR
1566 (val_normal_range IS NOT NULL)
1567 )
1568 ORDER BY
1569 CASE
1570 WHEN clin_when > %(clin_when)s THEN clin_when - %(clin_when)s
1571 ELSE %(clin_when)s - clin_when
1572 END
1573 LIMIT 1"""
1574 args = {
1575 'pk_type': self._payload[self._idx['pk_test_type']],
1576 'unit': self._payload[self._idx['val_unit']],
1577 'clin_when': self._payload[self._idx['clin_when']]
1578 }
1579 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
1580 if len(rows) == 0:
1581 return None
1582 return cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': rows[0]})
1583
1584 temporally_closest_normal_range = property(_get_temporally_closest_normal_range, lambda x:x)
1585
1586
1646
1647 formatted_range = property(_get_formatted_range, lambda x:x)
1648
1649
1651 return cMeasurementType(aPK_obj = self._payload[self._idx['pk_test_type']])
1652
1653 test_type = property(_get_test_type, lambda x:x)
1654
1655
1657
1658 if self._payload[self._idx['is_technically_abnormal']] is False:
1659 return False
1660
1661 indicator = self._payload[self._idx['abnormality_indicator']]
1662 if indicator is not None:
1663 indicator = indicator.strip()
1664 if indicator != '':
1665 if indicator.strip('+') == '':
1666 return True
1667 if indicator.strip('-') == '':
1668 return False
1669
1670 if self._payload[self._idx['val_num']] is None:
1671 return None
1672
1673 target_max = self._payload[self._idx['val_target_max']]
1674 if target_max is not None:
1675 if target_max < self._payload[self._idx['val_num']]:
1676 return True
1677
1678 normal_max = self._payload[self._idx['val_normal_max']]
1679 if normal_max is not None:
1680 if normal_max < self._payload[self._idx['val_num']]:
1681 return True
1682 return None
1683
1684 is_considered_elevated = property(_get_is_considered_elevated, lambda x:x)
1685
1686
1688
1689 if self._payload[self._idx['is_technically_abnormal']] is False:
1690 return False
1691
1692 indicator = self._payload[self._idx['abnormality_indicator']]
1693 if indicator is not None:
1694 indicator = indicator.strip()
1695 if indicator != '':
1696 if indicator.strip('+') == '':
1697 return False
1698 if indicator.strip('-') == '':
1699 return True
1700
1701 if self._payload[self._idx['val_num']] is None:
1702 return None
1703
1704 target_min = self._payload[self._idx['val_target_min']]
1705 if target_min is not None:
1706 if target_min > self._payload[self._idx['val_num']]:
1707 return True
1708
1709 normal_min = self._payload[self._idx['val_normal_min']]
1710 if normal_min is not None:
1711 if normal_min > self._payload[self._idx['val_num']]:
1712 return True
1713 return None
1714
1715 is_considered_lowered = property(_get_is_considered_lowered, lambda x:x)
1716
1717
1726
1727 is_considered_abnormal = property(_get_is_considered_abnormal, lambda x:x)
1728
1729
1731 """Parse reference range from string.
1732
1733 Note: does NOT save the result.
1734 """
1735 ref_range = ref_range.strip().replace(' ', '')
1736
1737 is_range = regex.match('-{0,1}\d+[.,]{0,1}\d*--{0,1}\d+[.,]{0,1}\d*$', ref_range, regex.UNICODE)
1738 if is_range is not None:
1739 min_val = regex.match('-{0,1}\d+[.,]{0,1}\d*-', ref_range, regex.UNICODE).group(0).rstrip('-')
1740 success, min_val = gmTools.input2decimal(min_val)
1741 max_val = (regex.search('--{0,1}\d+[.,]{0,1}\d*$', ref_range, regex.UNICODE).group(0))[1:]
1742 success, max_val = gmTools.input2decimal(max_val)
1743 self['val_normal_min'] = min_val
1744 self['val_normal_max'] = max_val
1745 return
1746
1747 if ref_range.startswith('<'):
1748 is_range = regex.match('<\d+[.,]{0,1}\d*$', ref_range, regex.UNICODE)
1749 if is_range is not None:
1750 max_val = ref_range[1:]
1751 success, max_val = gmTools.input2decimal(max_val)
1752 self['val_normal_min'] = 0
1753 self['val_normal_max'] = max_val
1754 return
1755
1756 if ref_range.startswith('<-'):
1757 is_range = regex.match('<-\d+[.,]{0,1}\d*$', ref_range, regex.UNICODE)
1758 if is_range is not None:
1759 max_val = ref_range[1:]
1760 success, max_val = gmTools.input2decimal(max_val)
1761 self['val_normal_min'] = None
1762 self['val_normal_max'] = max_val
1763 return
1764
1765 if ref_range.startswith('>'):
1766 is_range = regex.match('>\d+[.,]{0,1}\d*$', ref_range, regex.UNICODE)
1767 if is_range is not None:
1768 min_val = ref_range[1:]
1769 success, min_val = gmTools.input2decimal(min_val)
1770 self['val_normal_min'] = min_val
1771 self['val_normal_max'] = None
1772 return
1773
1774 if ref_range.startswith('>-'):
1775 is_range = regex.match('>-\d+[.,]{0,1}\d*$', ref_range, regex.UNICODE)
1776 if is_range is not None:
1777 min_val = ref_range[1:]
1778 success, min_val = gmTools.input2decimal(min_val)
1779 self['val_normal_min'] = min_val
1780 self['val_normal_max'] = 0
1781 return
1782
1783 self['val_normal_range'] = ref_range
1784 return
1785
1786 reference_range = property(lambda x:x, _set_reference_range)
1787
1788
1825
1826 formatted_abnormality_indicator = property(_get_formatted_abnormality_indicator, lambda x:x)
1827
1828
1830 if self._payload[self._idx['val_alpha']] is None:
1831 return False
1832 lines = gmTools.strip_empty_lines(text = self._payload[self._idx['val_alpha']], eol = '\n', return_list = True)
1833 if len(lines) > 4:
1834 return True
1835 return False
1836
1837 is_long_text = property(_get_is_long_text, lambda x:x)
1838
1839
1841 if self._payload[self._idx['val_alpha']] is None:
1842 return None
1843 val = self._payload[self._idx['val_alpha']].lstrip()
1844 if val[0] == '<':
1845 factor = decimal.Decimal(0.5)
1846 val = val[1:]
1847 elif val[0] == '>':
1848 factor = 2
1849 val = val[1:]
1850 else:
1851 return None
1852 success, val = gmTools.input2decimal(initial = val)
1853 if not success:
1854 return None
1855 return val * factor
1856
1857 estimate_numeric_value_from_alpha = property(_get_estimate_numeric_value_from_alpha, lambda x:x)
1858
1859
1860 - def set_review(self, technically_abnormal=None, clinically_relevant=None, comment=None, make_me_responsible=False):
1861
1862
1863 if self._payload[self._idx['reviewed']]:
1864 self.__change_existing_review (
1865 technically_abnormal = technically_abnormal,
1866 clinically_relevant = clinically_relevant,
1867 comment = comment
1868 )
1869 else:
1870
1871
1872 if technically_abnormal is None:
1873 if clinically_relevant is None:
1874 comment = gmTools.none_if(comment, '', strip_string = True)
1875 if comment is None:
1876 if make_me_responsible is False:
1877 return True
1878 self.__set_new_review (
1879 technically_abnormal = technically_abnormal,
1880 clinically_relevant = clinically_relevant,
1881 comment = comment
1882 )
1883
1884 if make_me_responsible is True:
1885 cmd = "SELECT pk FROM dem.staff WHERE db_user = current_user"
1886 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd}])
1887 self['pk_intended_reviewer'] = rows[0][0]
1888 self.save_payload()
1889 return
1890
1891 self.refetch_payload()
1892
1893
1894 - def get_adjacent_results(self, desired_earlier_results=1, desired_later_results=1, max_offset=None):
1895
1896 if desired_earlier_results < 1:
1897 raise ValueError('<desired_earlier_results> must be > 0')
1898
1899 if desired_later_results < 1:
1900 raise ValueError('<desired_later_results> must be > 0')
1901
1902 args = {
1903 'pat': self._payload[self._idx['pk_patient']],
1904 'ttyp': self._payload[self._idx['pk_test_type']],
1905 'tloinc': self._payload[self._idx['loinc_tt']],
1906 'mtyp': self._payload[self._idx['pk_meta_test_type']],
1907 'mloinc': self._payload[self._idx['loinc_meta']],
1908 'when': self._payload[self._idx['clin_when']],
1909 'offset': max_offset
1910 }
1911 WHERE = '((pk_test_type = %(ttyp)s) OR (loinc_tt = %(tloinc)s))'
1912 WHERE_meta = '((pk_meta_test_type = %(mtyp)s) OR (loinc_meta = %(mloinc)s))'
1913 if max_offset is not None:
1914 WHERE = WHERE + ' AND (clin_when BETWEEN (%(when)s - %(offset)s) AND (%(when)s + %(offset)s))'
1915 WHERE_meta = WHERE_meta + ' AND (clin_when BETWEEN (%(when)s - %(offset)s) AND (%(when)s + %(offset)s))'
1916
1917 SQL = """
1918 SELECT * FROM clin.v_test_results
1919 WHERE
1920 pk_patient = %%(pat)s
1921 AND
1922 clin_when %s %%(when)s
1923 AND
1924 %s
1925 ORDER BY clin_when
1926 LIMIT %s"""
1927
1928
1929 earlier_results = []
1930
1931 cmd = SQL % ('<', WHERE, desired_earlier_results)
1932 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
1933 if len(rows) > 0:
1934 earlier_results.extend([ cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': r}) for r in rows ])
1935
1936 missing_results = desired_earlier_results - len(earlier_results)
1937 if missing_results > 0:
1938 cmd = SQL % ('<', WHERE_meta, missing_results)
1939 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
1940 if len(rows) > 0:
1941 earlier_results.extend([ cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': r}) for r in rows ])
1942
1943
1944 later_results = []
1945
1946 cmd = SQL % ('>', WHERE, desired_later_results)
1947 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
1948 if len(rows) > 0:
1949 later_results.extend([ cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': r}) for r in rows ])
1950
1951 missing_results = desired_later_results - len(later_results)
1952 if missing_results > 0:
1953 cmd = SQL % ('>', WHERE_meta, missing_results)
1954 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
1955 if len(rows) > 0:
1956 later_results.extend([ cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': r}) for r in rows ])
1957
1958 return earlier_results, later_results
1959
1960
1961
1962
1963 - def __set_new_review(self, technically_abnormal=None, clinically_relevant=None, comment=None):
1964 """Add a review to a row.
1965
1966 - if technically abnormal is not provided/None it will be set
1967 to True if the lab's indicator has a meaningful value
1968 - if clinically relevant is not provided/None it is set to
1969 whatever technically abnormal is
1970 """
1971 if technically_abnormal is None:
1972 technically_abnormal = False
1973 if self._payload[self._idx['abnormality_indicator']] is not None:
1974 if self._payload[self._idx['abnormality_indicator']].strip() != '':
1975 technically_abnormal = True
1976
1977 if clinically_relevant is None:
1978 clinically_relevant = technically_abnormal
1979
1980 cmd = """
1981 INSERT INTO clin.reviewed_test_results (
1982 fk_reviewed_row,
1983 is_technically_abnormal,
1984 clinically_relevant,
1985 comment
1986 ) VALUES (
1987 %(pk)s,
1988 %(abnormal)s,
1989 %(relevant)s,
1990 gm.nullify_empty_string(%(cmt)s)
1991 )"""
1992 args = {
1993 'pk': self._payload[self._idx['pk_test_result']],
1994 'abnormal': technically_abnormal,
1995 'relevant': clinically_relevant,
1996 'cmt': comment
1997 }
1998
1999 gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
2000
2001
2003 """Change a review on a row.
2004
2005 - if technically abnormal/clinically relevant are
2006 None they are not set
2007 """
2008 args = {
2009 'pk_result': self._payload[self._idx['pk_test_result']],
2010 'abnormal': technically_abnormal,
2011 'relevant': clinically_relevant,
2012 'cmt': comment
2013 }
2014
2015 set_parts = [
2016 'fk_reviewer = (SELECT pk FROM dem.staff WHERE db_user = current_user)',
2017 'comment = gm.nullify_empty_string(%(cmt)s)'
2018 ]
2019
2020 if technically_abnormal is not None:
2021 set_parts.append('is_technically_abnormal = %(abnormal)s')
2022
2023 if clinically_relevant is not None:
2024 set_parts.append('clinically_relevant = %(relevant)s')
2025
2026 cmd = """
2027 UPDATE clin.reviewed_test_results SET
2028 %s
2029 WHERE
2030 fk_reviewed_row = %%(pk_result)s
2031 """ % ',\n '.join(set_parts)
2032
2033 gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
2034
2035
2036 -def get_test_results(pk_patient=None, encounters=None, episodes=None, order_by=None, return_pks=False):
2037
2038 where_parts = []
2039
2040 if pk_patient is not None:
2041 where_parts.append('pk_patient = %(pat)s')
2042 args = {'pat': pk_patient}
2043
2044
2045
2046
2047
2048 if encounters is not None:
2049 where_parts.append('pk_encounter IN %(encs)s')
2050 args['encs'] = tuple(encounters)
2051
2052 if episodes is not None:
2053 where_parts.append('pk_episode IN %(epis)s')
2054 args['epis'] = tuple(episodes)
2055
2056 if order_by is None:
2057 order_by = ''
2058 else:
2059 order_by = 'ORDER BY %s' % order_by
2060
2061 cmd = """
2062 SELECT * FROM clin.v_test_results
2063 WHERE %s
2064 %s
2065 """ % (
2066 ' AND '.join(where_parts),
2067 order_by
2068 )
2069 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
2070 if return_pks:
2071 return [ r['pk_test_result'] for r in rows ]
2072 tests = [ cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': r}) for r in rows ]
2073 return tests
2074
2075
2077
2078 if order_by is None:
2079 order_by = ''
2080 else:
2081 order_by = 'ORDER BY %s' % order_by
2082
2083 args = {
2084 'pat': pk_patient,
2085 'pk_pnl': pk_panel
2086 }
2087
2088 if group_by_meta_type:
2089
2090
2091
2092 cmd = """
2093 SELECT c_vtr.*
2094 FROM (
2095 -- max(clin_when) per test_type-in-panel for patient
2096 SELECT
2097 pk_meta_test_type,
2098 MAX(clin_when) AS max_clin_when
2099 FROM clin.v_test_results
2100 WHERE
2101 pk_patient = %(pat)s
2102 AND
2103 pk_meta_test_type IS DISTINCT FROM NULL
2104 AND
2105 pk_test_type IN (
2106 (SELECT c_vtt4tp.pk_test_type FROM clin.v_test_types4test_panel c_vtt4tp WHERE c_vtt4tp.pk_test_panel = %(pk_pnl)s)
2107 )
2108 GROUP BY pk_meta_test_type
2109 ) AS latest_results
2110 INNER JOIN clin.v_test_results c_vtr ON
2111 c_vtr.pk_meta_test_type = latest_results.pk_meta_test_type
2112 AND
2113 c_vtr.clin_when = latest_results.max_clin_when
2114
2115 UNION ALL
2116
2117 SELECT c_vtr.*
2118 FROM (
2119 -- max(clin_when) per test_type-in-panel for patient
2120 SELECT
2121 pk_test_type,
2122 MAX(clin_when) AS max_clin_when
2123 FROM clin.v_test_results
2124 WHERE
2125 pk_patient = %(pat)s
2126 AND
2127 pk_meta_test_type IS NULL
2128 AND
2129 pk_test_type IN (
2130 (SELECT c_vtt4tp.pk_test_type FROM clin.v_test_types4test_panel c_vtt4tp WHERE c_vtt4tp.pk_test_panel = %(pk_pnl)s)
2131 )
2132 GROUP BY pk_test_type
2133 ) AS latest_results
2134 INNER JOIN clin.v_test_results c_vtr ON
2135 c_vtr.pk_test_type = latest_results.pk_test_type
2136 AND
2137 c_vtr.clin_when = latest_results.max_clin_when
2138 """
2139 else:
2140
2141
2142
2143 cmd = """
2144 SELECT c_vtr.*
2145 FROM (
2146 -- max(clin_when) per test_type-in-panel for patient
2147 SELECT
2148 pk_test_type,
2149 MAX(clin_when) AS max_clin_when
2150 FROM clin.v_test_results
2151 WHERE
2152 pk_patient = %(pat)s
2153 AND
2154 pk_test_type IN (
2155 (SELECT c_vtt4tp.pk_test_type FROM clin.v_test_types4test_panel c_vtt4tp WHERE c_vtt4tp.pk_test_panel = %(pk_pnl)s)
2156 )
2157 GROUP BY pk_test_type
2158 ) AS latest_results
2159 -- this INNER join makes certain we do not expand
2160 -- the row selection beyond the patient's rows
2161 -- which we constrained to inside the SELECT
2162 -- producing "latest_results"
2163 INNER JOIN clin.v_test_results c_vtr ON
2164 c_vtr.pk_test_type = latest_results.pk_test_type
2165 AND
2166 c_vtr.clin_when = latest_results.max_clin_when
2167 """
2168 cmd += order_by
2169 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
2170 return [ cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': r}) for r in rows ]
2171
2172
2174
2175 if None not in [test_type, loinc]:
2176 raise ValueError('either <test_type> or <loinc> must be None')
2177
2178 args = {
2179 'pat': patient,
2180 'ttyp': test_type,
2181 'loinc': loinc,
2182 'ts': timestamp,
2183 'intv': tolerance_interval
2184 }
2185
2186 where_parts = ['pk_patient = %(pat)s']
2187 if test_type is not None:
2188 where_parts.append('pk_test_type = %(ttyp)s')
2189 elif loinc is not None:
2190 where_parts.append('((loinc_tt IN %(loinc)s) OR (loinc_meta IN %(loinc)s))')
2191 args['loinc'] = tuple(loinc)
2192
2193 if tolerance_interval is None:
2194 where_parts.append('clin_when = %(ts)s')
2195 else:
2196 where_parts.append('clin_when between (%(ts)s - %(intv)s::interval) AND (%(ts)s + %(intv)s::interval)')
2197
2198 cmd = """
2199 SELECT * FROM clin.v_test_results
2200 WHERE
2201 %s
2202 ORDER BY
2203 abs(extract(epoch from age(clin_when, %%(ts)s)))
2204 LIMIT 1""" % ' AND '.join(where_parts)
2205
2206 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
2207 if len(rows) == 0:
2208 return None
2209
2210 return cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': rows[0]})
2211
2212
2214
2215 args = {
2216 'pat': patient,
2217 'ts': timestamp
2218 }
2219
2220 where_parts = [
2221 'pk_patient = %(pat)s',
2222 "date_trunc('day'::text, clin_when) = date_trunc('day'::text, %(ts)s)"
2223 ]
2224
2225 cmd = """
2226 SELECT * FROM clin.v_test_results
2227 WHERE
2228 %s
2229 ORDER BY
2230 val_grouping,
2231 abbrev_tt,
2232 clin_when DESC
2233 """ % ' AND '.join(where_parts)
2234 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
2235 return [ cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': r}) for r in rows ]
2236
2237
2239 args = {'pk_issue': pk_health_issue}
2240 where_parts = ['pk_health_issue = %(pk_issue)s']
2241 cmd = """
2242 SELECT * FROM clin.v_test_results
2243 WHERE %s
2244 ORDER BY
2245 val_grouping,
2246 abbrev_tt,
2247 clin_when DESC
2248 """ % ' AND '.join(where_parts)
2249 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
2250 return [ cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': r}) for r in rows ]
2251
2252
2254 args = {'pk_epi': pk_episode}
2255 where_parts = ['pk_episode = %(pk_epi)s']
2256 cmd = """
2257 SELECT * FROM clin.v_test_results
2258 WHERE %s
2259 ORDER BY
2260 val_grouping,
2261 abbrev_tt,
2262 clin_when DESC
2263 """ % ' AND '.join(where_parts)
2264 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
2265 return [ cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': r}) for r in rows ]
2266
2267
2269 """Get N most recent results *among* a list of tests selected by LOINC."""
2270
2271
2272
2273
2274 if no_of_results < 1:
2275 raise ValueError('<no_of_results> must be > 0')
2276
2277
2278
2279
2280
2281
2282
2283
2284 args = {'pat': patient, 'loincs': tuple(loincs)}
2285 if max_age is None:
2286 max_age_cond = ''
2287 else:
2288 max_age_cond = 'AND clin_when > (now() - %(max_age)s::interval)'
2289 args['max_age'] = max_age
2290
2291 if consider_meta_type:
2292 rank_order = '_rank ASC'
2293 else:
2294 rank_order = '_rank DESC'
2295
2296 cmd = """
2297 SELECT DISTINCT ON (pk_test_type) * FROM (
2298 ( -- get results for meta type loincs
2299 SELECT *, 1 AS _rank
2300 FROM clin.v_test_results
2301 WHERE
2302 pk_patient = %%(pat)s
2303 AND
2304 loinc_meta IN %%(loincs)s
2305 %s
2306 -- no use weeding out duplicates by UNION-only, because _rank will make them unique anyway
2307 ) UNION ALL (
2308 -- get results for direct loincs
2309 SELECT *, 2 AS _rank
2310 FROM clin.v_test_results
2311 WHERE
2312 pk_patient = %%(pat)s
2313 AND
2314 loinc_tt IN %%(loincs)s
2315 %s
2316 )
2317 ORDER BY
2318 -- all of them by most-recent
2319 clin_when DESC,
2320 -- then by rank-of meta vs direct
2321 %s
2322 ) AS ordered_results
2323 -- then return only what's needed
2324 LIMIT %s""" % (
2325 max_age_cond,
2326 max_age_cond,
2327 rank_order,
2328 no_of_results
2329 )
2330 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
2331 if no_of_results == 1:
2332 if len(rows) == 0:
2333 return None
2334 return cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': rows[0]})
2335 return [ cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': r}) for r in rows ]
2336
2337
2339 """Get N most recent results for *a* (one) given test type."""
2340
2341 assert (test_type is not None), '<test_type> must not be None'
2342 assert (no_of_results > 0), '<no_of_results> must be > 0'
2343
2344 args = {
2345 'pat': patient,
2346 'ttyp': test_type
2347 }
2348 where_parts = ['pk_patient = %(pat)s']
2349 where_parts.append('pk_test_type = %(ttyp)s')
2350 cmd = """
2351 SELECT * FROM clin.v_test_results
2352 WHERE
2353 %s
2354 ORDER BY clin_when DESC
2355 LIMIT %s""" % (
2356 ' AND '.join(where_parts),
2357 no_of_results
2358 )
2359 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
2360 if no_of_results == 1:
2361 if len(rows) == 0:
2362 return None
2363 return cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': rows[0]})
2364 return [ cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': r}) for r in rows ]
2365
2366
2368 """Return the one most recent result for *each* of a list of test types."""
2369
2370 where_parts = ['pk_patient = %(pat)s']
2371 args = {'pat': pk_patient}
2372
2373 if pk_test_types is not None:
2374 where_parts.append('pk_test_type IN %(ttyps)s')
2375 args['ttyps'] = tuple(pk_test_types)
2376
2377 if consider_meta_type:
2378 partition = 'PARTITION BY pk_patient, pk_meta_test_type'
2379 else:
2380 partition = 'PARTITION BY pk_patient, pk_test_type'
2381
2382 cmd = """
2383 SELECT * FROM (
2384 SELECT
2385 *,
2386 MIN(clin_when) OVER relevant_tests AS min_clin_when
2387 FROM
2388 clin.v_test_results
2389 WHERE
2390 %s
2391 --WINDOW relevant_tests AS (PARTITION BY pk_patient, pk_test_type)
2392 --WINDOW relevant_tests AS (PARTITION BY pk_patient, pk_meta_test_type)
2393 WINDOW relevant_tests AS (%s)
2394 ) AS windowed_tests
2395 WHERE
2396 clin_when = min_clin_when
2397 """ % (
2398 ' AND '.join(where_parts),
2399 partition
2400 )
2401 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
2402 if return_pks:
2403 return [ r['pk_test_result'] for r in rows ]
2404 return [ cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': r}) for r in rows ]
2405
2406
2408 """Get N most recent results for a given patient."""
2409
2410 if no_of_results < 1:
2411 raise ValueError('<no_of_results> must be > 0')
2412
2413 args = {'pat': patient}
2414 cmd = """
2415 SELECT * FROM clin.v_test_results
2416 WHERE
2417 pk_patient = %%(pat)s
2418 ORDER BY clin_when DESC
2419 LIMIT %s""" % no_of_results
2420 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
2421 return [ cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': r}) for r in rows ]
2422
2423
2425
2426 if None not in [test_type, loinc]:
2427 raise ValueError('either <test_type> or <loinc> must be None')
2428
2429 args = {
2430 'pat': patient,
2431 'ttyp': test_type,
2432 'loinc': loinc
2433 }
2434
2435 where_parts = ['pk_patient = %(pat)s']
2436 if test_type is not None:
2437 where_parts.append('pk_test_type = %(ttyp)s')
2438 elif loinc is not None:
2439 where_parts.append('((loinc_tt IN %(loinc)s) OR (loinc_meta IN %(loinc)s))')
2440 args['loinc'] = tuple(loinc)
2441
2442 cmd = """
2443 SELECT * FROM clin.v_test_results
2444 WHERE
2445 %s
2446 ORDER BY clin_when
2447 LIMIT 1""" % ' AND '.join(where_parts)
2448 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
2449 if len(rows) == 0:
2450 return None
2451
2452 return cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': rows[0]})
2453
2454
2456 try:
2457 pk = int(result)
2458 except (TypeError, AttributeError):
2459 pk = result['pk_test_result']
2460
2461 cmd = 'DELETE FROM clin.test_result WHERE pk = %(pk)s'
2462 gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': {'pk': pk}}])
2463
2464
2465 -def create_test_result(encounter=None, episode=None, type=None, intended_reviewer=None, val_num=None, val_alpha=None, unit=None, link_obj=None):
2466
2467 cmd1 = """
2468 INSERT INTO clin.test_result (
2469 fk_encounter,
2470 fk_episode,
2471 fk_type,
2472 fk_intended_reviewer,
2473 val_num,
2474 val_alpha,
2475 val_unit
2476 ) VALUES (
2477 %(enc)s,
2478 %(epi)s,
2479 %(type)s,
2480 %(rev)s,
2481 %(v_num)s,
2482 %(v_alpha)s,
2483 %(unit)s
2484 )
2485 """
2486 cmd2 = "SELECT * from clin.v_test_results WHERE pk_test_result = currval(pg_get_serial_sequence('clin.test_result', 'pk'))"
2487 args = {
2488 'enc': encounter,
2489 'epi': episode,
2490 'type': type,
2491 'rev': intended_reviewer,
2492 'v_num': val_num,
2493 'v_alpha': val_alpha,
2494 'unit': unit
2495 }
2496 rows, idx = gmPG2.run_rw_queries (
2497 link_obj = link_obj,
2498 queries = [
2499 {'cmd': cmd1, 'args': args},
2500 {'cmd': cmd2}
2501 ],
2502 return_data = True,
2503 get_col_idx = True
2504 )
2505 tr = cTestResult(row = {
2506 'pk_field': 'pk_test_result',
2507 'idx': idx,
2508 'data': rows[0]
2509 })
2510 return tr
2511
2512
2523
2524
2525 -def __tests2latex_minipage(results=None, width='1.5cm', show_time=False, show_range=True):
2526
2527 if len(results) == 0:
2528 return '\\begin{minipage}{%s} \\end{minipage}' % width
2529
2530 lines = []
2531 for t in results:
2532
2533 tmp = ''
2534
2535 if show_time:
2536 tmp += '{\\tiny (%s)} ' % t['clin_when'].strftime('%H:%M')
2537
2538 tmp += '%.8s' % t['unified_val']
2539
2540 lines.append(tmp)
2541 tmp = ''
2542
2543 if show_range:
2544 has_range = (
2545 t['unified_target_range'] is not None
2546 or
2547 t['unified_target_min'] is not None
2548 or
2549 t['unified_target_max'] is not None
2550 )
2551 if has_range:
2552 if t['unified_target_range'] is not None:
2553 tmp += '{\\tiny %s}' % t['unified_target_range']
2554 else:
2555 tmp += '{\\tiny %s}' % (
2556 gmTools.coalesce(t['unified_target_min'], '- ', '%s - '),
2557 gmTools.coalesce(t['unified_target_max'], '', '%s')
2558 )
2559 lines.append(tmp)
2560
2561 return '\\begin{minipage}{%s} \\begin{flushright} %s \\end{flushright} \\end{minipage}' % (width, ' \\\\ '.join(lines))
2562
2563
2565
2566 if len(results) == 0:
2567 return ''
2568
2569 lines = []
2570 for t in results:
2571
2572 tmp = ''
2573
2574 if show_time:
2575 tmp += '\\tiny %s ' % t['clin_when'].strftime('%H:%M')
2576
2577 tmp += '\\normalsize %.8s' % t['unified_val']
2578
2579 lines.append(tmp)
2580 tmp = '\\tiny %s' % gmTools.coalesce(t['val_unit'], '', '%s ')
2581
2582 if not show_range:
2583 lines.append(tmp)
2584 continue
2585
2586 has_range = (
2587 t['unified_target_range'] is not None
2588 or
2589 t['unified_target_min'] is not None
2590 or
2591 t['unified_target_max'] is not None
2592 )
2593
2594 if not has_range:
2595 lines.append(tmp)
2596 continue
2597
2598 if t['unified_target_range'] is not None:
2599 tmp += '[%s]' % t['unified_target_range']
2600 else:
2601 tmp += '[%s%s]' % (
2602 gmTools.coalesce(t['unified_target_min'], '--', '%s--'),
2603 gmTools.coalesce(t['unified_target_max'], '', '%s')
2604 )
2605 lines.append(tmp)
2606
2607 return ' \\\\ '.join(lines)
2608
2609
2685
2686
2688
2689 if filename is None:
2690 filename = gmTools.get_unique_filename(prefix = 'gm2gpl-', suffix = '.dat')
2691
2692
2693 series = {}
2694 for r in results:
2695 try:
2696 series[r['unified_name']].append(r)
2697 except KeyError:
2698 series[r['unified_name']] = [r]
2699
2700 gp_data = io.open(filename, mode = 'wt', encoding = 'utf8')
2701
2702 gp_data.write('# %s\n' % _('GNUmed test results export for Gnuplot plotting'))
2703 gp_data.write('# -------------------------------------------------------------\n')
2704 gp_data.write('# first line of index: test type abbreviation & name\n')
2705 gp_data.write('#\n')
2706 gp_data.write('# clin_when at full precision\n')
2707 gp_data.write('# value\n')
2708 gp_data.write('# unit\n')
2709 gp_data.write('# unified (target or normal) range: lower bound\n')
2710 gp_data.write('# unified (target or normal) range: upper bound\n')
2711 gp_data.write('# normal range: lower bound\n')
2712 gp_data.write('# normal range: upper bound\n')
2713 gp_data.write('# target range: lower bound\n')
2714 gp_data.write('# target range: upper bound\n')
2715 gp_data.write('# clin_when formatted into string as x-axis tic label\n')
2716 gp_data.write('# -------------------------------------------------------------\n')
2717
2718 for test_type in series.keys():
2719 if len(series[test_type]) == 0:
2720 continue
2721
2722 r = series[test_type][0]
2723 title = '%s (%s)' % (
2724 r['unified_abbrev'],
2725 r['unified_name']
2726 )
2727 gp_data.write('\n\n"%s" "%s"\n' % (title, title))
2728
2729 prev_date = None
2730 prev_year = None
2731 for r in series[test_type]:
2732 curr_date = gmDateTime.pydt_strftime(r['clin_when'], '%Y-%m-%d', 'utf8', gmDateTime.acc_days)
2733 if curr_date == prev_date:
2734 gp_data.write('\n# %s\n' % _('blank line inserted to allow for discontinued line drawing of same-day values'))
2735 if show_year:
2736 if r['clin_when'].year == prev_year:
2737 when_template = '%b %d %H:%M'
2738 else:
2739 when_template = '%b %d %H:%M (%Y)'
2740 prev_year = r['clin_when'].year
2741 else:
2742 when_template = '%b %d'
2743 val = r['val_num']
2744 if val is None:
2745 val = r.estimate_numeric_value_from_alpha
2746 if val is None:
2747 continue
2748 gp_data.write ('%s %s "%s" %s %s %s %s %s %s "%s"\n' % (
2749
2750 gmDateTime.pydt_strftime(r['clin_when'], '%Y-%m-%d_%H:%M', 'utf8', gmDateTime.acc_minutes),
2751 val,
2752 gmTools.coalesce(r['val_unit'], '"<?>"'),
2753 gmTools.coalesce(r['unified_target_min'], '"<?>"'),
2754 gmTools.coalesce(r['unified_target_max'], '"<?>"'),
2755 gmTools.coalesce(r['val_normal_min'], '"<?>"'),
2756 gmTools.coalesce(r['val_normal_max'], '"<?>"'),
2757 gmTools.coalesce(r['val_target_min'], '"<?>"'),
2758 gmTools.coalesce(r['val_target_max'], '"<?>"'),
2759 gmDateTime.pydt_strftime (
2760 r['clin_when'],
2761 format = when_template,
2762 accuracy = gmDateTime.acc_minutes
2763 )
2764 ))
2765 prev_date = curr_date
2766
2767 gp_data.close()
2768
2769 return filename
2770
2771
2772 -class cLabResult(gmBusinessDBObject.cBusinessDBObject):
2773 """Represents one lab result."""
2774
2775 _cmd_fetch_payload = """
2776 select *, xmin_test_result from v_results4lab_req
2777 where pk_result=%s"""
2778 _cmds_lock_rows_for_update = [
2779 """select 1 from test_result where pk=%(pk_result)s and xmin=%(xmin_test_result)s for update"""
2780 ]
2781 _cmds_store_payload = [
2782 """update test_result set
2783 clin_when = %(val_when)s,
2784 narrative = %(progress_note_result)s,
2785 fk_type = %(pk_test_type)s,
2786 val_num = %(val_num)s::numeric,
2787 val_alpha = %(val_alpha)s,
2788 val_unit = %(val_unit)s,
2789 val_normal_min = %(val_normal_min)s,
2790 val_normal_max = %(val_normal_max)s,
2791 val_normal_range = %(val_normal_range)s,
2792 val_target_min = %(val_target_min)s,
2793 val_target_max = %(val_target_max)s,
2794 val_target_range = %(val_target_range)s,
2795 abnormality_indicator = %(abnormal)s,
2796 norm_ref_group = %(ref_group)s,
2797 note_provider = %(note_provider)s,
2798 material = %(material)s,
2799 material_detail = %(material_detail)s
2800 where pk = %(pk_result)s""",
2801 """select xmin_test_result from v_results4lab_req where pk_result=%(pk_result)s"""
2802 ]
2803
2804 _updatable_fields = [
2805 'val_when',
2806 'progress_note_result',
2807 'val_num',
2808 'val_alpha',
2809 'val_unit',
2810 'val_normal_min',
2811 'val_normal_max',
2812 'val_normal_range',
2813 'val_target_min',
2814 'val_target_max',
2815 'val_target_range',
2816 'abnormal',
2817 'ref_group',
2818 'note_provider',
2819 'material',
2820 'material_detail'
2821 ]
2822
2823 - def __init__(self, aPK_obj=None, row=None):
2824 """Instantiate.
2825
2826 aPK_obj as dict:
2827 - patient_id
2828 - when_field (see view definition)
2829 - when
2830 - test_type
2831 - val_num
2832 - val_alpha
2833 - unit
2834 """
2835
2836 if aPK_obj is None:
2837 gmBusinessDBObject.cBusinessDBObject.__init__(self, row=row)
2838 return
2839 pk = aPK_obj
2840
2841 if type(aPK_obj) == dict:
2842
2843 if None in [aPK_obj['patient_id'], aPK_obj['when'], aPK_obj['when_field'], aPK_obj['test_type'], aPK_obj['unit']]:
2844 raise gmExceptions.ConstructorError('parameter error: %s' % aPK_obj)
2845 if (aPK_obj['val_num'] is None) and (aPK_obj['val_alpha'] is None):
2846 raise gmExceptions.ConstructorError('parameter error: val_num and val_alpha cannot both be None')
2847
2848 where_snippets = [
2849 'pk_patient=%(patient_id)s',
2850 'pk_test_type=%(test_type)s',
2851 '%s=%%(when)s' % aPK_obj['when_field'],
2852 'val_unit=%(unit)s'
2853 ]
2854 if aPK_obj['val_num'] is not None:
2855 where_snippets.append('val_num=%(val_num)s::numeric')
2856 if aPK_obj['val_alpha'] is not None:
2857 where_snippets.append('val_alpha=%(val_alpha)s')
2858
2859 where_clause = ' and '.join(where_snippets)
2860 cmd = "select pk_result from v_results4lab_req where %s" % where_clause
2861 data = gmPG.run_ro_query('historica', cmd, None, aPK_obj)
2862 if data is None:
2863 raise gmExceptions.ConstructorError('error getting lab result for: %s' % aPK_obj)
2864 if len(data) == 0:
2865 raise gmExceptions.NoSuchClinItemError('no lab result for: %s' % aPK_obj)
2866 pk = data[0][0]
2867
2868 gmBusinessDBObject.cBusinessDBObject.__init__(self, aPK_obj=pk)
2869
2871 cmd = """
2872 select
2873 %s,
2874 vbp.title,
2875 vbp.firstnames,
2876 vbp.lastnames,
2877 vbp.dob
2878 from v_active_persons vbp
2879 where vbp.pk_identity = %%s""" % self._payload[self._idx['pk_patient']]
2880 pat = gmPG.run_ro_query('historica', cmd, None, self._payload[self._idx['pk_patient']])
2881 return pat[0]
2882
2883
2884 -class cLabRequest(gmBusinessDBObject.cBusinessDBObject):
2885 """Represents one lab request."""
2886
2887 _cmd_fetch_payload = """
2888 select *, xmin_lab_request from v_lab_requests
2889 where pk_request=%s"""
2890 _cmds_lock_rows_for_update = [
2891 """select 1 from lab_request where pk=%(pk_request)s and xmin=%(xmin_lab_request)s for update"""
2892 ]
2893 _cmds_store_payload = [
2894 """update lab_request set
2895 request_id=%(request_id)s,
2896 lab_request_id=%(lab_request_id)s,
2897 clin_when=%(sampled_when)s,
2898 lab_rxd_when=%(lab_rxd_when)s,
2899 results_reported_when=%(results_reported_when)s,
2900 request_status=%(request_status)s,
2901 is_pending=%(is_pending)s::bool,
2902 narrative=%(progress_note)s
2903 where pk=%(pk_request)s""",
2904 """select xmin_lab_request from v_lab_requests where pk_request=%(pk_request)s"""
2905 ]
2906 _updatable_fields = [
2907 'request_id',
2908 'lab_request_id',
2909 'sampled_when',
2910 'lab_rxd_when',
2911 'results_reported_when',
2912 'request_status',
2913 'is_pending',
2914 'progress_note'
2915 ]
2916
2917 - def __init__(self, aPK_obj=None, row=None):
2918 """Instantiate lab request.
2919
2920 The aPK_obj can be either a dict with the keys "req_id"
2921 and "lab" or a simple primary key.
2922 """
2923
2924 if aPK_obj is None:
2925 gmBusinessDBObject.cBusinessDBObject.__init__(self, row=row)
2926 return
2927 pk = aPK_obj
2928
2929 if type(aPK_obj) == dict:
2930
2931 try:
2932 aPK_obj['req_id']
2933 aPK_obj['lab']
2934 except:
2935 _log.exception('[%s:??]: faulty <aPK_obj> structure: [%s]' % (self.__class__.__name__, aPK_obj), sys.exc_info())
2936 raise gmExceptions.ConstructorError('[%s:??]: cannot derive PK from [%s]' % (self.__class__.__name__, aPK_obj))
2937
2938 where_snippets = []
2939 vals = {}
2940 where_snippets.append('request_id=%(req_id)s')
2941 if type(aPK_obj['lab']) == int:
2942 where_snippets.append('pk_test_org=%(lab)s')
2943 else:
2944 where_snippets.append('lab_name=%(lab)s')
2945 where_clause = ' and '.join(where_snippets)
2946 cmd = "select pk_request from v_lab_requests where %s" % where_clause
2947
2948 data = gmPG.run_ro_query('historica', cmd, None, aPK_obj)
2949 if data is None:
2950 raise gmExceptions.ConstructorError('[%s:??]: error getting lab request for [%s]' % (self.__class__.__name__, aPK_obj))
2951 if len(data) == 0:
2952 raise gmExceptions.NoSuchClinItemError('[%s:??]: no lab request for [%s]' % (self.__class__.__name__, aPK_obj))
2953 pk = data[0][0]
2954
2955 gmBusinessDBObject.cBusinessDBObject.__init__(self, aPK_obj=pk)
2956
2958 cmd = """
2959 select vpi.pk_patient, vbp.title, vbp.firstnames, vbp.lastnames, vbp.dob
2960 from v_pat_items vpi, v_active_persons vbp
2961 where
2962 vpi.pk_item=%s
2963 and
2964 vbp.pk_identity=vpi.pk_patient"""
2965 pat = gmPG.run_ro_query('historica', cmd, None, self._payload[self._idx['pk_item']])
2966 if pat is None:
2967 _log.error('cannot get patient for lab request [%s]' % self._payload[self._idx['pk_item']])
2968 return None
2969 if len(pat) == 0:
2970 _log.error('no patient associated with lab request [%s]' % self._payload[self._idx['pk_item']])
2971 return None
2972 return pat[0]
2973
2974
2975
2976
2977 -def create_lab_request(lab=None, req_id=None, pat_id=None, encounter_id=None, episode_id=None):
2978 """Create or get lab request.
2979
2980 returns tuple (status, value):
2981 (True, lab request instance)
2982 (False, error message)
2983 (None, housekeeping_todo primary key)
2984 """
2985 req = None
2986 aPK_obj = {
2987 'lab': lab,
2988 'req_id': req_id
2989 }
2990 try:
2991 req = cLabRequest (aPK_obj)
2992 except gmExceptions.NoSuchClinItemError as msg:
2993 _log.info('%s: will try to create lab request' % str(msg))
2994 except gmExceptions.ConstructorError as msg:
2995 _log.exception(str(msg), sys.exc_info(), verbose=0)
2996 return (False, msg)
2997
2998 if req is not None:
2999 db_pat = req.get_patient()
3000 if db_pat is None:
3001 _log.error('cannot cross-check patient on lab request')
3002 return (None, '')
3003
3004 if pat_id != db_pat[0]:
3005 _log.error('lab request found for [%s:%s] but patient mismatch: expected [%s], in DB [%s]' % (lab, req_id, pat_id, db_pat))
3006 me = '$RCSfile: gmPathLab.py,v $ $Revision: 1.81 $'
3007 to = 'user'
3008 prob = _('The lab request already exists but belongs to a different patient.')
3009 sol = _('Verify which patient this lab request really belongs to.')
3010 ctxt = _('lab [%s], request ID [%s], expected link with patient [%s], currently linked to patient [%s]') % (lab, req_id, pat_id, db_pat)
3011 cat = 'lab'
3012 status, data = gmPG.add_housekeeping_todo(me, to, prob, sol, ctxt, cat)
3013 return (None, data)
3014 return (True, req)
3015
3016 queries = []
3017 if type(lab) is int:
3018 cmd = "insert into lab_request (fk_encounter, fk_episode, fk_test_org, request_id) values (%s, %s, %s, %s)"
3019 else:
3020 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)"
3021 queries.append((cmd, [encounter_id, episode_id, str(lab), req_id]))
3022 cmd = "select currval('lab_request_pk_seq')"
3023 queries.append((cmd, []))
3024
3025 result, err = gmPG.run_commit('historica', queries, True)
3026 if result is None:
3027 return (False, err)
3028 try:
3029 req = cLabRequest(aPK_obj=result[0][0])
3030 except gmExceptions.ConstructorError as msg:
3031 _log.exception(str(msg), sys.exc_info(), verbose=0)
3032 return (False, msg)
3033 return (True, req)
3034
3035 -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):
3036 tres = None
3037 data = {
3038 'patient_id': patient_id,
3039 'when_field': when_field,
3040 'when': when,
3041 'test_type': test_type,
3042 'val_num': val_num,
3043 'val_alpha': val_alpha,
3044 'unit': unit
3045 }
3046 try:
3047 tres = cLabResult(aPK_obj=data)
3048
3049 _log.error('will not overwrite existing test result')
3050 _log.debug(str(tres))
3051 return (None, tres)
3052 except gmExceptions.NoSuchClinItemError:
3053 _log.debug('test result not found - as expected, will create it')
3054 except gmExceptions.ConstructorError as msg:
3055 _log.exception(str(msg), sys.exc_info(), verbose=0)
3056 return (False, msg)
3057 if request is None:
3058 return (False, _('need lab request when inserting lab result'))
3059
3060 if encounter_id is None:
3061 encounter_id = request['pk_encounter']
3062 queries = []
3063 cmd = "insert into test_result (fk_encounter, fk_episode, fk_type, val_num, val_alpha, val_unit) values (%s, %s, %s, %s, %s, %s)"
3064 queries.append((cmd, [encounter_id, request['pk_episode'], test_type, val_num, val_alpha, unit]))
3065 cmd = "insert into lnk_result2lab_req (fk_result, fk_request) values ((select currval('test_result_pk_seq')), %s)"
3066 queries.append((cmd, [request['pk_request']]))
3067 cmd = "select currval('test_result_pk_seq')"
3068 queries.append((cmd, []))
3069
3070 result, err = gmPG.run_commit('historica', queries, True)
3071 if result is None:
3072 return (False, err)
3073 try:
3074 tres = cLabResult(aPK_obj=result[0][0])
3075 except gmExceptions.ConstructorError as msg:
3076 _log.exception(str(msg), sys.exc_info(), verbose=0)
3077 return (False, msg)
3078 return (True, tres)
3079
3081
3082 if limit < 1:
3083 limit = 1
3084
3085 lim = limit + 1
3086 cmd = """
3087 select pk_result
3088 from v_results4lab_req
3089 where reviewed is false
3090 order by pk_patient
3091 limit %s""" % lim
3092 rows = gmPG.run_ro_query('historica', cmd)
3093 if rows is None:
3094 _log.error('error retrieving unreviewed lab results')
3095 return (None, _('error retrieving unreviewed lab results'))
3096 if len(rows) == 0:
3097 return (False, [])
3098
3099 if len(rows) == lim:
3100 more_avail = True
3101
3102 del rows[limit]
3103 else:
3104 more_avail = False
3105 results = []
3106 for row in rows:
3107 try:
3108 results.append(cLabResult(aPK_obj=row[0]))
3109 except gmExceptions.ConstructorError:
3110 _log.exception('skipping unreviewed lab result [%s]' % row[0], sys.exc_info(), verbose=0)
3111 return (more_avail, results)
3112
3113
3115 lim = limit + 1
3116 cmd = "select pk from lab_request where is_pending is true limit %s" % lim
3117 rows = gmPG.run_ro_query('historica', cmd)
3118 if rows is None:
3119 _log.error('error retrieving pending lab requests')
3120 return (None, None)
3121 if len(rows) == 0:
3122 return (False, [])
3123 results = []
3124
3125 if len(rows) == lim:
3126 too_many = True
3127
3128 del rows[limit]
3129 else:
3130 too_many = False
3131 requests = []
3132 for row in rows:
3133 try:
3134 requests.append(cLabRequest(aPK_obj=row[0]))
3135 except gmExceptions.ConstructorError:
3136 _log.exception('skipping pending lab request [%s]' % row[0], sys.exc_info(), verbose=0)
3137 return (too_many, requests)
3138
3139
3141 """Get logically next request ID for given lab.
3142
3143 - incrementor_func:
3144 - if not supplied the next ID is guessed
3145 - if supplied it is applied to the most recently used ID
3146 """
3147 if type(lab) == int:
3148 lab_snippet = 'vlr.fk_test_org=%s'
3149 else:
3150 lab_snippet = 'vlr.lab_name=%s'
3151 lab = str(lab)
3152 cmd = """
3153 select request_id
3154 from lab_request lr0
3155 where lr0.clin_when = (
3156 select max(vlr.sampled_when)
3157 from v_lab_requests vlr
3158 where %s
3159 )""" % lab_snippet
3160 rows = gmPG.run_ro_query('historica', cmd, None, lab)
3161 if rows is None:
3162 _log.warning('error getting most recently used request ID for lab [%s]' % lab)
3163 return ''
3164 if len(rows) == 0:
3165 return ''
3166 most_recent = rows[0][0]
3167
3168 if incrementor_func is not None:
3169 try:
3170 next = incrementor_func(most_recent)
3171 except TypeError:
3172 _log.error('cannot call incrementor function [%s]' % str(incrementor_func))
3173 return most_recent
3174 return next
3175
3176 for pos in range(len(most_recent)):
3177 header = most_recent[:pos]
3178 trailer = most_recent[pos:]
3179 try:
3180 return '%s%s' % (header, str(int(trailer) + 1))
3181 except ValueError:
3182 header = most_recent[:-1]
3183 trailer = most_recent[-1:]
3184 return '%s%s' % (header, chr(ord(trailer) + 1))
3185
3186
3188 """Calculate BMI.
3189
3190 mass: kg
3191 height: cm
3192 age: not yet used
3193
3194 returns:
3195 (True/False, data)
3196 True: data = (bmi, lower_normal, upper_normal)
3197 False: data = error message
3198 """
3199 converted, mass = gmTools.input2decimal(mass)
3200 if not converted:
3201 return False, 'mass: cannot convert <%s> to Decimal' % mass
3202
3203 converted, height = gmTools.input2decimal(height)
3204 if not converted:
3205 return False, 'height: cannot convert <%s> to Decimal' % height
3206
3207 approx_surface = (height / decimal.Decimal(100))**2
3208 bmi = mass / approx_surface
3209
3210 print(mass, height, '->', approx_surface, '->', bmi)
3211
3212 lower_normal_mass = 20.0 * approx_surface
3213 upper_normal_mass = 25.0 * approx_surface
3214
3215 return True, (bmi, lower_normal_mass, upper_normal_mass)
3216
3217
3218
3219
3220 if __name__ == '__main__':
3221
3222 if len(sys.argv) < 2:
3223 sys.exit()
3224
3225 if sys.argv[1] != 'test':
3226 sys.exit()
3227
3228 import time
3229
3230 gmI18N.activate_locale()
3231 gmI18N.install_domain()
3232
3233
3235 tr = create_test_result (
3236 encounter = 1,
3237 episode = 1,
3238 type = 1,
3239 intended_reviewer = 1,
3240 val_num = '12',
3241 val_alpha=None,
3242 unit = 'mg/dl'
3243 )
3244 print(tr)
3245 return tr
3246
3250
3258
3260 print("test_result()")
3261
3262 data = {
3263 'patient_id': 12,
3264 'when_field': 'val_when',
3265 'when': '2000-09-17 18:23:00+02',
3266 'test_type': 9,
3267 'val_num': 17.3,
3268 'val_alpha': None,
3269 'unit': 'mg/l'
3270 }
3271 lab_result = cLabResult(aPK_obj=data)
3272 print(lab_result)
3273 fields = lab_result.get_fields()
3274 for field in fields:
3275 print(field, ':', lab_result[field])
3276 print("updatable:", lab_result.get_updatable_fields())
3277 print(time.time())
3278 print(lab_result.get_patient())
3279 print(time.time())
3280
3282 print("test_request()")
3283 try:
3284
3285
3286 data = {
3287 'req_id': 'EML#SC937-0176-CEC#11',
3288 'lab': 'Enterprise Main Lab'
3289 }
3290 lab_req = cLabRequest(aPK_obj=data)
3291 except gmExceptions.ConstructorError as msg:
3292 print("no such lab request:", msg)
3293 return
3294 print(lab_req)
3295 fields = lab_req.get_fields()
3296 for field in fields:
3297 print(field, ':', lab_req[field])
3298 print("updatable:", lab_req.get_updatable_fields())
3299 print(time.time())
3300 print(lab_req.get_patient())
3301 print(time.time())
3302
3307
3312
3320
3325
3330
3339
3341 done, data = calculate_bmi(mass = sys.argv[2], height = sys.argv[3])
3342 bmi, low, high = data
3343 print("BMI:", bmi)
3344 print("low:", low, "kg")
3345 print("hi :", high, "kg")
3346
3347
3353
3354
3356 tp = cTestPanel(aPK_obj = 1)
3357
3358
3359 print(tp.format())
3360
3361
3362
3363 most_recent = tp.get_most_recent_results(pk_patient = 12, group_by_meta_type = True, include_missing = True)
3364
3365 print('found:', len(most_recent))
3366
3367 for t in most_recent:
3368 print('--------------')
3369 if t['pk_meta_test_type'] is None:
3370 print("standalone")
3371 else:
3372 print("meta")
3373 print(t.format())
3374
3375
3377 most_recent = get_most_recent_results_in_loinc_group (
3378
3379 loincs = ['8867-4'],
3380 no_of_results = 2,
3381 patient = 12,
3382 consider_meta_type = True
3383
3384 )
3385 for t in most_recent:
3386 if t['pk_meta_test_type'] is None:
3387 print("---- standalone ----")
3388 else:
3389 print("---- meta ----")
3390 print(t.format())
3391
3392
3393
3394
3395
3396
3397
3398
3399
3400
3401
3402
3403
3404
3405
3406
3407
3408 test_get_most_recent_results_for_panel()
3409
3410
3411
3412