1 """GNUmed clinical narrative business object."""
2
3 __author__ = "Carlos Moro <cfmoro1976@yahoo.es>, Karsten Hilbert <Karsten.Hilbert@gmx.net>"
4 __license__ = 'GPL v2 or later (for details see http://gnu.org)'
5
6 import sys
7 import logging
8
9
10 if __name__ == '__main__':
11 sys.path.insert(0, '../../')
12 from Gnumed.pycommon import gmPG2
13 from Gnumed.pycommon import gmBusinessDBObject
14 from Gnumed.pycommon import gmTools
15 from Gnumed.pycommon import gmDispatcher
16 from Gnumed.pycommon import gmHooks
17 from Gnumed.pycommon import gmDateTime
18
19 from Gnumed.business import gmCoding
20 from Gnumed.business import gmSoapDefs
21 from Gnumed.business import gmAutoHints
22
23
24 _log = logging.getLogger('gm.emr')
25
26
30
31 gmDispatcher.connect(_on_soap_modified, 'clin.clin_narrative_mod_db')
32
33
34 -class cNarrative(gmBusinessDBObject.cBusinessDBObject):
35 """Represents one clinical free text entry."""
36
37 _cmd_fetch_payload = "SELECT * FROM clin.v_narrative WHERE pk_narrative = %s"
38 _cmds_store_payload = [
39 """update clin.clin_narrative set
40 narrative = %(narrative)s,
41 clin_when = %(date)s,
42 soap_cat = lower(%(soap_cat)s),
43 fk_encounter = %(pk_encounter)s,
44 fk_episode = %(pk_episode)s
45 WHERE
46 pk = %(pk_narrative)s
47 AND
48 xmin = %(xmin_clin_narrative)s
49 RETURNING
50 xmin AS xmin_clin_narrative"""
51 ]
52
53 _updatable_fields = [
54 'narrative',
55 'date',
56 'soap_cat',
57 'pk_episode',
58 'pk_encounter'
59 ]
60
61
64
65
92
93
95 """<pk_code> must be a value from ref.coding_system_root.pk_coding_system (clin.lnk_code2item_root.fk_generic_code)"""
96
97 if pk_code in self._payload[self._idx['pk_generic_codes']]:
98 return
99
100 cmd = """
101 INSERT INTO clin.lnk_code2narrative
102 (fk_item, fk_generic_code)
103 SELECT
104 %(item)s,
105 %(code)s
106 WHERE NOT EXISTS (
107 SELECT 1 FROM clin.lnk_code2narrative
108 WHERE
109 fk_item = %(item)s
110 AND
111 fk_generic_code = %(code)s
112 )"""
113 args = {
114 'item': self._payload[self._idx['pk_narrative']],
115 'code': pk_code
116 }
117 rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
118 return
119
120
122 """<pk_code> must be a value from ref.coding_system_root.pk_coding_system (clin.lnk_code2item_root.fk_generic_code)"""
123 cmd = "DELETE FROM clin.lnk_code2narrative WHERE fk_item = %(item)s AND fk_generic_code = %(code)s"
124 args = {
125 'item': self._payload[self._idx['pk_narrative']],
126 'code': pk_code
127 }
128 rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
129 return True
130
131
132
133
135 if len(self._payload[self._idx['pk_generic_codes']]) == 0:
136 return []
137
138 cmd = gmCoding._SQL_get_generic_linked_codes % 'pk_generic_code IN %(pks)s'
139 args = {'pks': tuple(self._payload[self._idx['pk_generic_codes']])}
140 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
141 return [ gmCoding.cGenericLinkedCode(row = {'data': r, 'idx': idx, 'pk_field': 'pk_lnk_code2item'}) for r in rows ]
142
144 queries = []
145
146 if len(self._payload[self._idx['pk_generic_codes']]) > 0:
147 queries.append ({
148 'cmd': 'DELETE FROM clin.lnk_code2narrative WHERE fk_item = %(narr)s AND fk_generic_code IN %(codes)s',
149 'args': {
150 'narr': self._payload[self._idx['pk_narrative']],
151 'codes': tuple(self._payload[self._idx['pk_generic_codes']])
152 }
153 })
154
155 for pk_code in pk_codes:
156 queries.append ({
157 'cmd': 'INSERT INTO clin.lnk_code2narrative (fk_item, fk_generic_code) VALUES (%(narr)s, %(pk_code)s)',
158 'args': {
159 'narr': self._payload[self._idx['pk_narrative']],
160 'pk_code': pk_code
161 }
162 })
163 if len(queries) == 0:
164 return
165
166 rows, idx = gmPG2.run_rw_queries(queries = queries)
167 return
168
169 generic_codes = property(_get_generic_codes, _set_generic_codes)
170
171
173 """Create clinical narrative entries.
174
175 <soap>
176 must be a dict, the keys being SOAP categories (including U and
177 None=admin) and the values being text (possibly multi-line)
178
179 Existing but empty ('' or None) categories are skipped.
180 """
181 if soap is None:
182 return True
183
184 if not gmSoapDefs.are_valid_soap_cats(soap.keys(), allow_upper = True):
185 raise ValueError('invalid SOAP category in <soap> dictionary: %s', soap)
186
187 if link_obj is None:
188 link_obj = gmPG2.get_connection(readonly = False)
189 conn_rollback = link_obj.rollback
190 conn_commit = link_obj.commit
191 conn_close = link_obj.close
192 else:
193 conn_rollback = lambda x:x
194 conn_commit = lambda x:x
195 conn_close = lambda x:x
196
197 instances = {}
198 for cat in soap:
199 val = soap[cat]
200 if val is None:
201 continue
202 if ''.join([ v.strip() for v in val ]) == '':
203 continue
204 instance = create_narrative_item (
205 narrative = '\n'.join([ v.strip() for v in val ]),
206 soap_cat = cat,
207 episode_id = episode_id,
208 encounter_id = encounter_id,
209 link_obj = link_obj
210 )
211 if instance is None:
212 continue
213 instances[cat] = instance
214
215 conn_commit()
216 conn_close()
217 return instances
218
219
220 -def create_narrative_item(narrative=None, soap_cat=None, episode_id=None, encounter_id=None, link_obj=None):
221 """Creates a new clinical narrative entry
222
223 narrative - free text clinical narrative
224 soap_cat - soap category
225 episode_id - episodes's primary key
226 encounter_id - encounter's primary key
227
228 any of the args being None (except soap_cat) will fail the SQL code
229 """
230
231 narrative = narrative.strip()
232 if narrative == '':
233 return None
234
235 args = {'enc': encounter_id, 'epi': episode_id, 'soap': soap_cat, 'narr': narrative}
236
237
238
239
240
241 cmd = """
242 INSERT INTO clin.clin_narrative
243 (fk_encounter, fk_episode, narrative, soap_cat)
244 SELECT
245 %(enc)s, %(epi)s, %(narr)s, lower(%(soap)s)
246 WHERE NOT EXISTS (
247 SELECT 1 FROM clin.v_narrative
248 WHERE
249 pk_encounter = %(enc)s
250 AND
251 pk_episode = %(epi)s
252 AND
253 soap_cat = lower(%(soap)s)
254 AND
255 narrative = %(narr)s
256 )
257 RETURNING pk"""
258 rows, idx = gmPG2.run_rw_queries(link_obj = link_obj, queries = [{'cmd': cmd, 'args': args}], return_data = True, get_col_idx = False)
259 if len(rows) == 1:
260
261 return cNarrative(aPK_obj = rows[0]['pk'], link_obj = link_obj)
262
263 if len(rows) > 1:
264 raise Exception('more than one row returned from single-row INSERT')
265
266
267 cmd = """
268 SELECT * FROM clin.v_narrative
269 WHERE
270 pk_encounter = %(enc)s
271 AND
272 pk_episode = %(epi)s
273 AND
274 soap_cat = lower(%(soap)s)
275 AND
276 narrative = %(narr)s
277 """
278 rows, idx = gmPG2.run_ro_queries(link_obj = link_obj, queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
279 if len(rows) == 1:
280 return cNarrative(row = {'pk_field': 'pk_narrative', 'data': rows[0], 'idx': idx})
281
282 raise Exception('retrieving known-to-exist narrative row returned 0 or >1 result: %s' % len(rows))
283
284
286 """Deletes a clin.clin_narrative row by it's PK."""
287 cmd = "DELETE FROM clin.clin_narrative WHERE pk=%s"
288 rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': [narrative]}])
289 return True
290
291
292 -def get_narrative(since=None, until=None, encounters=None, episodes=None, issues=None, soap_cats=None, providers=None, patient=None, order_by=None):
293 """Get SOAP notes pertinent to this encounter.
294
295 since
296 - initial date for narrative items
297 until
298 - final date for narrative items
299 encounters
300 - list of encounters whose narrative are to be retrieved
301 episodes
302 - list of episodes whose narrative are to be retrieved
303 issues
304 - list of health issues whose narrative are to be retrieved
305 soap_cats
306 - list of SOAP categories of the narrative to be retrieved
307 """
308 where_parts = ['TRUE']
309 args = {}
310
311 if encounters is not None:
312 where_parts.append('pk_encounter IN %(encs)s')
313 args['encs'] = tuple(encounters)
314
315 if episodes is not None:
316 where_parts.append('pk_episode IN %(epis)s')
317 args['epis'] = tuple(episodes)
318
319 if issues is not None:
320 where_parts.append('pk_health_issue IN %(issues)s')
321 args['issues'] = tuple(issues)
322
323 if patient is not None:
324 where_parts.append('pk_patient = %(pat)s')
325 args['pat'] = patient
326
327 if soap_cats is not None:
328 where_parts.append('c_vn.soap_cat IN %(soap_cats)s')
329 args['soap_cats'] = tuple(soap_cats)
330
331 if order_by is None:
332 order_by = 'ORDER BY date, soap_rank'
333 else:
334 order_by = 'ORDER BY %s' % order_by
335
336 cmd = """
337 SELECT
338 c_vn.*,
339 c_scr.rank AS soap_rank
340 FROM
341 clin.v_narrative c_vn
342 LEFT JOIN clin.soap_cat_ranks c_scr ON c_vn.soap_cat = c_scr.soap_cat
343 WHERE
344 %s
345 %s
346 """ % (
347 ' AND '.join(where_parts),
348 order_by
349 )
350
351 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
352
353 filtered_narrative = [ cNarrative(row = {'pk_field': 'pk_narrative', 'idx': idx, 'data': row}) for row in rows ]
354
355 if since is not None:
356 filtered_narrative = [ narr for narr in filtered_narrative if narr['date'] >= since ]
357
358 if until is not None:
359 filtered_narrative = [ narr for narr in filtered_narrative if narr['date'] < until ]
360
361 if providers is not None:
362 filtered_narrative = [ narr for narr in filtered_narrative if narr['modified_by'] in providers ]
363
364 return filtered_narrative
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380 -def get_as_journal(since=None, until=None, encounters=None, episodes=None, issues=None, soap_cats=None, providers=None, order_by=None, time_range=None, patient=None, active_encounter=None):
381
382 if (patient is None) and (episodes is None) and (issues is None) and (encounters is None):
383 raise ValueError('at least one of <patient>, <episodes>, <issues>, <encounters> must not be None')
384
385 if order_by is None:
386 order_by = 'ORDER BY clin_when, pk_episode, scr, modified_when, src_table'
387 else:
388 order_by = 'ORDER BY %s' % order_by
389
390 where_parts = []
391 args = {}
392
393 if patient is not None:
394 where_parts.append('c_vej.pk_patient = %(pat)s')
395 args['pat'] = patient
396
397 if soap_cats is not None:
398
399
400 if None in soap_cats:
401 where_parts.append('((c_vej.soap_cat IN %(soap_cat)s) OR (c_vej.soap_cat IS NULL))')
402 soap_cats.remove(None)
403 else:
404 where_parts.append('c_vej.soap_cat IN %(soap_cat)s')
405 args['soap_cat'] = tuple(soap_cats)
406
407 if time_range is not None:
408 where_parts.append("c_vej.clin_when > (now() - '%s days'::interval)" % time_range)
409
410 if episodes is not None:
411 where_parts.append("c_vej.pk_episode IN %(epis)s")
412 args['epis'] = tuple(episodes)
413
414 if issues is not None:
415 where_parts.append("c_vej.pk_health_issue IN %(issues)s")
416 args['issues'] = tuple(issues)
417
418
419
420 cmd_journal = """
421 SELECT
422 to_char(c_vej.clin_when, 'YYYY-MM-DD') AS date,
423 c_vej.clin_when,
424 coalesce(c_vej.soap_cat, '') as soap_cat,
425 c_vej.narrative,
426 c_vej.src_table,
427 c_scr.rank AS scr,
428 c_vej.modified_when,
429 to_char(c_vej.modified_when, 'YYYY-MM-DD HH24:MI') AS date_modified,
430 c_vej.modified_by,
431 c_vej.row_version,
432 c_vej.pk_episode,
433 c_vej.pk_encounter,
434 c_vej.soap_cat as real_soap_cat,
435 c_vej.src_pk,
436 c_vej.pk_health_issue,
437 c_vej.health_issue,
438 c_vej.episode,
439 c_vej.issue_active,
440 c_vej.issue_clinically_relevant,
441 c_vej.episode_open,
442 c_vej.encounter_started,
443 c_vej.encounter_last_affirmed,
444 c_vej.encounter_l10n_type,
445 c_vej.pk_patient
446 FROM
447 clin.v_emr_journal c_vej
448 join clin.soap_cat_ranks c_scr on (c_scr.soap_cat IS NOT DISTINCT FROM c_vej.soap_cat)
449 WHERE
450 %s
451 """ % '\n\t\t\t\t\tAND\n\t\t\t\t'.join(where_parts)
452
453 if active_encounter is None:
454 cmd = cmd_journal + '\n ' + order_by
455 else:
456 args['pk_enc'] = active_encounter['pk_encounter']
457 args['enc_start'] = active_encounter['started']
458 args['enc_last_affirmed'] = active_encounter['last_affirmed']
459 args['enc_type'] = active_encounter['l10n_type']
460 args['enc_pat'] = active_encounter['pk_patient']
461 cmd_hints = """
462 SELECT
463 to_char(now(), 'YYYY-MM-DD') AS date,
464 now() as clin_when,
465 'a'::text as soap_cat,
466 hints.title || E'\n' || hints.hint
467 as narrative,
468 'ref.auto_hint'::text as src_table,
469 c_scr.rank AS scr,
470 now() as modified_when,
471 to_char(now(), 'YYYY-MM-DD HH24:MI') AS date_modified,
472 current_user as modified_by,
473 0::integer as row_version,
474 NULL::integer as pk_episode,
475 %(pk_enc)s as pk_encounter,
476 'a'::text as real_soap_cat,
477 hints.pk_auto_hint as src_pk,
478 NULL::integer as pk_health_issue,
479 ''::text as health_issue,
480 ''::text as episode,
481 False as issue_active,
482 False as issue_clinically_relevant,
483 False as episode_open,
484 %(enc_start)s as encounter_started,
485 %(enc_last_affirmed)s as encounter_last_affirmed,
486 %(enc_type)s as encounter_l10n_type,
487 %(enc_pat)s as pk_patient
488 FROM
489 clin.get_hints_for_patient(%(enc_pat)s) as hints
490 join clin.soap_cat_ranks c_scr on (c_scr.soap_cat = 'a')
491 """
492 cmd = cmd_journal + '\nUNION ALL\n' + cmd_hints + '\n' + order_by
493
494 journal_rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
495
496 return journal_rows
497
498
499
500
501 -def search_text_across_emrs(search_term=None):
502
503 if search_term is None:
504 return []
505
506 if search_term.strip() == '':
507 return []
508
509 cmd = 'select * from clin.v_narrative4search where narrative ~* %(term)s order by pk_patient limit 1000'
510 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': {'term': search_term}}], get_col_idx = False)
511
512 return rows
513
514
515
516
517 if __name__ == '__main__':
518
519 if len(sys.argv) < 2:
520 sys.exit()
521
522 if sys.argv[1] != 'test':
523 sys.exit()
524
525 from Gnumed.pycommon import gmI18N
526 gmI18N.activate_locale()
527 gmI18N.install_domain(domain = 'gnumed')
528
529
531 print("\nnarrative test")
532 print("--------------")
533 narrative = cNarrative(aPK_obj=7)
534 fields = narrative.get_fields()
535 for field in fields:
536 print(field, ':', narrative[field])
537 print("updatable:", narrative.get_updatable_fields())
538 print("codes:", narrative.generic_codes)
539
540
541
542
543
544
545
546
547
549 results = search_text_across_emrs('cut')
550 for r in results:
551 print(r)
552
553
554
555 test_narrative()
556