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