Package Gnumed :: Package business :: Module gmClinNarrative
[frames] | no frames]

Source Code for Module Gnumed.business.gmClinNarrative

  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  #============================================================ 
27 -def _on_soap_modified():
28 """Always relates to the active patient.""" 29 gmHooks.run_hook_script(hook = 'after_soap_modified')
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 #--------------------------------------------------------
62 - def format_maximum_information(self, patient=None):
63 return self.format(fancy = True, width = 70).split('\n')
64 65 #--------------------------------------------------------
66 - def format(self, left_margin='', fancy=False, width=75):
67 68 if fancy: 69 txt = gmTools.wrap ( 70 text = _('%s: %s by %.8s (v%s)\n%s') % ( 71 self._payload[self._idx['date']].strftime('%x %H:%M'), 72 gmSoapDefs.soap_cat2l10n_str[self._payload[self._idx['soap_cat']]], 73 self._payload[self._idx['modified_by']], 74 self._payload[self._idx['row_version']], 75 self._payload[self._idx['narrative']] 76 ), 77 width = width, 78 initial_indent = '', 79 subsequent_indent = left_margin + ' ' 80 ) 81 else: 82 txt = '%s [%s]: %s (%.8s)' % ( 83 self._payload[self._idx['date']].strftime('%x %H:%M'), 84 gmSoapDefs.soap_cat2l10n[self._payload[self._idx['soap_cat']]], 85 self._payload[self._idx['narrative']], 86 self._payload[self._idx['modified_by']] 87 ) 88 if len(txt) > width: 89 txt = txt[:width] + gmTools.u_ellipsis 90 91 return txt
92 93 #--------------------------------------------------------
94 - def add_code(self, pk_code=None):
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 #--------------------------------------------------------
121 - def remove_code(self, pk_code=None):
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 # properties 133 #--------------------------------------------------------
134 - def _get_generic_codes(self):
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
143 - def _set_generic_codes(self, pk_codes):
144 queries = [] 145 # remove all codes 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 # add new codes 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 # run it all in one transaction 166 rows, idx = gmPG2.run_rw_queries(queries = queries) 167 return
168 169 generic_codes = property(_get_generic_codes, _set_generic_codes)
170 171 #============================================================
172 -def create_progress_note(soap=None, episode_id=None, encounter_id=None, link_obj=None):
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 # silently do not insert empty narrative 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 # insert new narrative 238 # but, also silently, do not insert true duplicates 239 # this should check for .provider = current_user but 240 # the view has provider mapped to their staff alias 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 # re-use same link_obj if given because when called from create_progress_note we won't yet see rows inside a new tx 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 # retrieve existing narrative 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 #------------------------------------------------------------
285 -def delete_clin_narrative(narrative=None):
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 # if issues is not None: 367 # filtered_narrative = (lambda narr: narr['pk_health_issue'] in issues, filtered_narrative) 368 # 369 # if episodes is not None: 370 # filtered_narrative = (lambda narr: narr['pk_episode'] in episodes, filtered_narrative) 371 # 372 # if encounters is not None: 373 # filtered_narrative = (lambda narr: narr['pk_encounter'] in encounters, filtered_narrative) 374 375 # if soap_cats is not None: 376 # soap_cats = map(lambda c: c.lower(), soap_cats) 377 # filtered_narrative = (lambda narr: narr['soap_cat'] in soap_cats, filtered_narrative) 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 # work around bug in psycopg2 not being able to properly 399 # adapt None to NULL inside tuples 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 # FIXME: implement more constraints 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 # convenience functions 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 # main 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 #-----------------------------------------
530 - def test_narrative():
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 #print "adding code..." 540 #narrative.add_code('Test code', 'Test coding system') 541 #print "codes:", diagnose.get_codes() 542 543 #print "creating narrative..." 544 #new_narrative = create_narrative_item(narrative = 'Test narrative', soap_cat = 'a', episode_id=1, encounter_id=2) 545 #print new_narrative 546 547 #-----------------------------------------
548 - def test_search_text_across_emrs():
549 results = search_text_across_emrs('cut') 550 for r in results: 551 print(r)
552 #----------------------------------------- 553 554 #test_search_text_across_emrs() 555 test_narrative() 556