Gnumed.business.gmClinNarrative

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