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, logging 
  7   
  8   
  9  if __name__ == '__main__': 
 10          sys.path.insert(0, '../../') 
 11  from Gnumed.pycommon import gmPG2, gmExceptions, gmBusinessDBObject, gmTools, gmDispatcher, gmHooks 
 12  from Gnumed.business import gmCoding 
 13   
 14   
 15  try: 
 16          _('dummy-no-need-to-translate-but-make-epydoc-happy') 
 17  except NameError: 
 18          _ = lambda x:x 
 19   
 20   
 21  _log = logging.getLogger('gm.emr') 
 22   
 23   
 24  soap_cat2l10n = { 
 25          u's': _('soap_S').replace(u'soap_', u''), 
 26          u'o': _('soap_O').replace(u'soap_', u''), 
 27          u'a': _('soap_A').replace(u'soap_', u''), 
 28          u'p': _('soap_P').replace(u'soap_', u''), 
 29          u'u': _('soap_U').replace(u'soap_', u''), 
 30  #       u'u': u'?', 
 31          None: gmTools.u_ellipsis, 
 32          u'': gmTools.u_ellipsis 
 33  } 
 34   
 35  soap_cat2l10n_str = { 
 36          u's': _('soap_Subjective').replace(u'soap_', u''), 
 37          u'o': _('soap_Objective').replace(u'soap_', u''), 
 38          u'a': _('soap_Assessment').replace(u'soap_', u''), 
 39          u'p': _('soap_Plan').replace(u'soap_', u''), 
 40          u'u': _('soap_Unspecified').replace(u'soap_', u''), 
 41          None: _('soap_Administrative').replace(u'soap_', u'') 
 42  } 
 43   
 44  l10n2soap_cat = { 
 45          _('soap_S').replace(u'soap_', u''): u's', 
 46          _('soap_O').replace(u'soap_', u''): u'o', 
 47          _('soap_A').replace(u'soap_', u''): u'a', 
 48          _('soap_P').replace(u'soap_', u''): u'p', 
 49          _('soap_U').replace(u'soap_', u''): u'u', 
 50  #       u'?': u'u', 
 51          gmTools.u_ellipsis: None 
 52  } 
 53   
 54  #============================================================ 
55 -def _on_soap_modified():
56 """Always relates to the active patient.""" 57 gmHooks.run_hook_script(hook = u'after_soap_modified')
58 59 gmDispatcher.connect(_on_soap_modified, u'clin.clin_narrative_mod_db') 60 61 #============================================================
62 -class cNarrative(gmBusinessDBObject.cBusinessDBObject):
63 """Represents one clinical free text entry. 64 """ 65 _cmd_fetch_payload = u"select *, xmin_clin_narrative from clin.v_pat_narrative where pk_narrative = %s" 66 _cmds_store_payload = [ 67 u"""update clin.clin_narrative set 68 narrative = %(narrative)s, 69 clin_when = %(date)s, 70 soap_cat = lower(%(soap_cat)s), 71 fk_encounter = %(pk_encounter)s 72 where 73 pk=%(pk_narrative)s and 74 xmin=%(xmin_clin_narrative)s""", 75 u"""select xmin_clin_narrative from clin.v_pat_narrative where pk_narrative=%(pk_narrative)s""" 76 ] 77 78 _updatable_fields = [ 79 'narrative', 80 'date', 81 'soap_cat', 82 'pk_episode', 83 'pk_encounter' 84 ] 85 86 #--------------------------------------------------------
87 - def format(self, left_margin=u'', fancy=False, width=75):
88 89 if fancy: 90 # FIXME: add revision 91 txt = gmTools.wrap ( 92 text = _('%s: %s by %.8s (v%s)\n%s') % ( 93 self._payload[self._idx['date']].strftime('%x %H:%M'), 94 soap_cat2l10n_str[self._payload[self._idx['soap_cat']]], 95 self._payload[self._idx['provider']], 96 self._payload[self._idx['row_version']], 97 self._payload[self._idx['narrative']] 98 ), 99 width = width, 100 initial_indent = u'', 101 subsequent_indent = left_margin + u' ' 102 ) 103 else: 104 txt = u'%s [%s]: %s (%.8s)' % ( 105 self._payload[self._idx['date']].strftime('%x %H:%M'), 106 soap_cat2l10n[self._payload[self._idx['soap_cat']]], 107 self._payload[self._idx['narrative']], 108 self._payload[self._idx['provider']] 109 ) 110 if len(txt) > width: 111 txt = txt[:width] + gmTools.u_ellipsis 112 113 return txt
114 #--------------------------------------------------------
115 - def add_code(self, pk_code=None):
116 """<pk_code> must be a value from ref.coding_system_root.pk_coding_system (clin.lnk_code2item_root.fk_generic_code)""" 117 118 if pk_code in self._payload[self._idx['pk_generic_codes']]: 119 return 120 121 cmd = u""" 122 INSERT INTO clin.lnk_code2narrative 123 (fk_item, fk_generic_code) 124 SELECT 125 %(item)s, 126 %(code)s 127 WHERE NOT EXISTS ( 128 SELECT 1 FROM clin.lnk_code2narrative 129 WHERE 130 fk_item = %(item)s 131 AND 132 fk_generic_code = %(code)s 133 )""" 134 args = { 135 'item': self._payload[self._idx['pk_narrative']], 136 'code': pk_code 137 } 138 rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}]) 139 return
140 #--------------------------------------------------------
141 - def remove_code(self, pk_code=None):
142 """<pk_code> must be a value from ref.coding_system_root.pk_coding_system (clin.lnk_code2item_root.fk_generic_code)""" 143 cmd = u"DELETE FROM clin.lnk_code2narrative WHERE fk_item = %(item)s AND fk_generic_code = %(code)s" 144 args = { 145 'item': self._payload[self._idx['pk_narrative']], 146 'code': pk_code 147 } 148 rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}]) 149 return True
150 #-------------------------------------------------------- 151 # properties 152 #--------------------------------------------------------
153 - def _get_generic_codes(self):
154 if len(self._payload[self._idx['pk_generic_codes']]) == 0: 155 return [] 156 157 cmd = gmCoding._SQL_get_generic_linked_codes % u'pk_generic_code IN %(pks)s' 158 args = {'pks': tuple(self._payload[self._idx['pk_generic_codes']])} 159 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True) 160 return [ gmCoding.cGenericLinkedCode(row = {'data': r, 'idx': idx, 'pk_field': 'pk_lnk_code2item'}) for r in rows ]
161
162 - def _set_generic_codes(self, pk_codes):
163 queries = [] 164 # remove all codes 165 if len(self._payload[self._idx['pk_generic_codes']]) > 0: 166 queries.append ({ 167 'cmd': u'DELETE FROM clin.lnk_code2narrative WHERE fk_item = %(narr)s AND fk_generic_code IN %(codes)s', 168 'args': { 169 'narr': self._payload[self._idx['pk_narrative']], 170 'codes': tuple(self._payload[self._idx['pk_generic_codes']]) 171 } 172 }) 173 # add new codes 174 for pk_code in pk_codes: 175 queries.append ({ 176 'cmd': u'INSERT INTO clin.lnk_code2narrative (fk_item, fk_generic_code) VALUES (%(narr)s, %(pk_code)s)', 177 'args': { 178 'narr': self._payload[self._idx['pk_narrative']], 179 'pk_code': pk_code 180 } 181 }) 182 if len(queries) == 0: 183 return 184 # run it all in one transaction 185 rows, idx = gmPG2.run_rw_queries(queries = queries) 186 return
187 188 generic_codes = property(_get_generic_codes, _set_generic_codes)
189 #============================================================ 190 # convenience functions 191 #============================================================
192 -def search_text_across_emrs(search_term=None):
193 194 if search_term is None: 195 return [] 196 197 if search_term.strip() == u'': 198 return [] 199 200 cmd = u'select * from clin.v_narrative4search where narrative ~* %(term)s order by pk_patient limit 1000' 201 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': {'term': search_term}}], get_col_idx = False) 202 203 return rows
204 #============================================================
205 -def create_clin_narrative(narrative=None, soap_cat=None, episode_id=None, encounter_id=None):
206 """Creates a new clinical narrative entry 207 208 narrative - free text clinical narrative 209 soap_cat - soap category 210 episode_id - episodes's primary key 211 encounter_id - encounter's primary key 212 """ 213 # any of the args being None (except soap_cat) should fail the SQL code 214 215 # sanity checks: 216 217 # 1) silently do not insert empty narrative 218 narrative = narrative.strip() 219 if narrative == u'': 220 return (True, None) 221 222 # 2) also, silently do not insert true duplicates 223 # FIXME: this should check for .provider = current_user but 224 # FIXME: the view has provider mapped to their staff alias 225 cmd = u""" 226 SELECT 227 *, xmin_clin_narrative 228 FROM clin.v_pat_narrative 229 WHERE 230 pk_encounter = %(enc)s 231 AND 232 pk_episode = %(epi)s 233 AND 234 soap_cat = %(soap)s 235 AND 236 narrative = %(narr)s 237 """ 238 args = { 239 'enc': encounter_id, 240 'epi': episode_id, 241 'soap': soap_cat, 242 'narr': narrative 243 } 244 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True) 245 if len(rows) == 1: 246 narrative = cNarrative(row = {'pk_field': 'pk_narrative', 'data': rows[0], 'idx': idx}) 247 return (True, narrative) 248 249 # insert new narrative 250 queries = [ 251 {'cmd': u""" 252 INSERT INTO clin.clin_narrative 253 (fk_encounter, fk_episode, narrative, soap_cat) 254 VALUES 255 (%s, %s, %s, lower(%s))""", 256 'args': [encounter_id, episode_id, narrative, soap_cat] 257 }, 258 {'cmd': u""" 259 SELECT *, xmin_clin_narrative 260 FROM clin.v_pat_narrative 261 WHERE 262 pk_narrative = currval(pg_get_serial_sequence('clin.clin_narrative', 'pk'))""" 263 } 264 ] 265 rows, idx = gmPG2.run_rw_queries(queries = queries, return_data = True, get_col_idx = True) 266 267 narrative = cNarrative(row = {'pk_field': 'pk_narrative', 'idx': idx, 'data': rows[0]}) 268 return (True, narrative)
269 #------------------------------------------------------------
270 -def delete_clin_narrative(narrative=None):
271 """Deletes a clin.clin_narrative row by it's PK.""" 272 cmd = u"delete from clin.clin_narrative where pk=%s" 273 rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': [narrative]}]) 274 return True
275 #------------------------------------------------------------
276 -def get_narrative(since=None, until=None, encounters=None, episodes=None, issues=None, soap_cats=None, providers=None, patient=None, order_by=None):
277 """Get SOAP notes pertinent to this encounter. 278 279 since 280 - initial date for narrative items 281 until 282 - final date for narrative items 283 encounters 284 - list of encounters whose narrative are to be retrieved 285 episodes 286 - list of episodes whose narrative are to be retrieved 287 issues 288 - list of health issues whose narrative are to be retrieved 289 soap_cats 290 - list of SOAP categories of the narrative to be retrieved 291 """ 292 where_parts = [u'TRUE'] 293 args = {} 294 295 if encounters is not None: 296 where_parts.append(u'pk_encounter IN %(encs)s') 297 args['encs'] = tuple(encounters) 298 299 if episodes is not None: 300 where_parts.append(u'pk_episode IN %(epis)s') 301 args['epis'] = tuple(episodes) 302 303 if issues is not None: 304 where_parts.append(u'pk_health_issue IN %(issues)s') 305 args['issues'] = tuple(issues) 306 307 if patient is not None: 308 where_parts.append(u'pk_patient = %(pat)s') 309 args['pat'] = patient 310 311 if soap_cats is not None: 312 where_parts.append(u'soap_cat IN %(soap_cats)s') 313 args['soap_cats'] = tuple(soap_cats) 314 315 if order_by is None: 316 order_by = u'ORDER BY date, soap_rank' 317 else: 318 order_by = u'ORDER BY %s' % order_by 319 320 cmd = u""" 321 SELECT 322 cvpn.*, 323 (SELECT rank FROM clin.soap_cat_ranks WHERE soap_cat = cvpn.soap_cat) 324 AS soap_rank 325 FROM 326 clin.v_pat_narrative cvpn 327 WHERE 328 %s 329 %s 330 """ % ( 331 u' AND '.join(where_parts), 332 order_by 333 ) 334 335 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True) 336 337 filtered_narrative = [ cNarrative(row = {'pk_field': 'pk_narrative', 'idx': idx, 'data': row}) for row in rows ] 338 339 if since is not None: 340 filtered_narrative = filter(lambda narr: narr['date'] >= since, filtered_narrative) 341 342 if until is not None: 343 filtered_narrative = filter(lambda narr: narr['date'] < until, filtered_narrative) 344 345 if providers is not None: 346 filtered_narrative = filter(lambda narr: narr['provider'] in providers, filtered_narrative) 347 348 return filtered_narrative
349 350 # if issues is not None: 351 # filtered_narrative = filter(lambda narr: narr['pk_health_issue'] in issues, filtered_narrative) 352 # 353 # if episodes is not None: 354 # filtered_narrative = filter(lambda narr: narr['pk_episode'] in episodes, filtered_narrative) 355 # 356 # if encounters is not None: 357 # filtered_narrative = filter(lambda narr: narr['pk_encounter'] in encounters, filtered_narrative) 358 359 # if soap_cats is not None: 360 # soap_cats = map(lambda c: c.lower(), soap_cats) 361 # filtered_narrative = filter(lambda narr: narr['soap_cat'] in soap_cats, filtered_narrative) 362 363 #------------------------------------------------------------
364 -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):
365 366 if (patient is None) and (episodes is None) and (issues is None) and (encounters is None): 367 raise ValueError('at least one of <patient>, <episodes>, <issues>, <encounters> must not be None') 368 369 if order_by is None: 370 order_by = u'ORDER BY vemrj.clin_when, vemrj.pk_episode, scr, vemrj.src_table' 371 else: 372 order_by = u'ORDER BY %s' % order_by 373 374 where_parts = [] 375 args = {} 376 377 if patient is not None: 378 where_parts.append(u'pk_patient = %(pat)s') 379 args['pat'] = patient 380 381 if soap_cats is not None: 382 # work around bug in psycopg2 not being able to properly 383 # adapt None to NULL inside tuples 384 if None in soap_cats: 385 where_parts.append(u'((vemrj.soap_cat IN %(soap_cat)s) OR (vemrj.soap_cat IS NULL))') 386 soap_cats.remove(None) 387 else: 388 where_parts.append(u'vemrj.soap_cat IN %(soap_cat)s') 389 args['soap_cat'] = tuple(soap_cats) 390 391 if time_range is not None: 392 where_parts.append(u"vemrj.clin_when > (now() - '%s days'::interval)" % time_range) 393 394 if episodes is not None: 395 where_parts.append(u"vemrj.pk_episode IN %(epis)s") 396 args['epis'] = tuple(episodes) 397 398 if issues is not None: 399 where_parts.append(u"vemrj.pk_health_issue IN %(issues)s") 400 args['issues'] = tuple(issues) 401 402 # FIXME: implement more constraints 403 404 cmd = u""" 405 SELECT 406 to_char(vemrj.clin_when, 'YYYY-MM-DD') AS date, 407 vemrj.clin_when, 408 coalesce(vemrj.soap_cat, '') as soap_cat, 409 vemrj.narrative, 410 vemrj.src_table, 411 412 (SELECT rank FROM clin.soap_cat_ranks WHERE soap_cat = vemrj.soap_cat) AS scr, 413 414 vemrj.modified_when, 415 to_char(vemrj.modified_when, 'YYYY-MM-DD HH24:MI') AS date_modified, 416 vemrj.modified_by, 417 vemrj.row_version, 418 vemrj.pk_episode, 419 vemrj.pk_encounter, 420 vemrj.soap_cat as real_soap_cat 421 FROM clin.v_emr_journal vemrj 422 WHERE 423 %s 424 %s""" % ( 425 u'\n\t\t\t\t\tAND\n\t\t\t\t'.join(where_parts), 426 order_by 427 ) 428 429 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True) 430 return rows
431 #============================================================ 432 # main 433 #------------------------------------------------------------ 434 if __name__ == '__main__': 435 436 if len(sys.argv) < 2: 437 sys.exit() 438 439 if sys.argv[1] != 'test': 440 sys.exit() 441 442 from Gnumed.pycommon import gmI18N 443 gmI18N.activate_locale() 444 gmI18N.install_domain(domain = 'gnumed') 445 446 #-----------------------------------------
447 - def test_narrative():
448 print "\nnarrative test" 449 print "--------------" 450 narrative = cNarrative(aPK_obj=7) 451 fields = narrative.get_fields() 452 for field in fields: 453 print field, ':', narrative[field] 454 print "updatable:", narrative.get_updatable_fields() 455 print "codes:", narrative.generic_codes
456 #print "adding code..." 457 #narrative.add_code('Test code', 'Test coding system') 458 #print "codes:", diagnose.get_codes() 459 460 #print "creating narrative..." 461 #status, new_narrative = create_clin_narrative(narrative = 'Test narrative', soap_cat = 'a', episode_id=1, encounter_id=2) 462 #print new_narrative 463 464 #-----------------------------------------
465 - def test_search_text_across_emrs():
466 results = search_text_across_emrs('cut') 467 for r in results: 468 print r
469 #----------------------------------------- 470 471 #test_search_text_across_emrs() 472 test_narrative() 473 474 #============================================================ 475