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

# Source Code for Module Gnumed.business.gmClinNarrative

      1  """GNUmed clinical narrative business object."""
      2  #============================================================
      3  __version__ = "$Revision: 1.45 $"
      4  __author__ = "Carlos Moro <cfmoro1976@yahoo.es>, Karsten Hilbert <Karsten.Hilbert@gmx.net>"
      5  __license__ = 'GPL v2 or later (for details see http://gnu.org)'
      6
      7  import sys, logging
      8
      9
     10  if __name__ == '__main__':
     11          sys.path.insert(0, '../../')
     12  from Gnumed.pycommon import gmPG2, gmExceptions, gmBusinessDBObject, gmTools, gmDispatcher, gmHooks
     13  from Gnumed.business import gmCoding
     14
     15
     16  try:
     17          _('dummy-no-need-to-translate-but-make-epydoc-happy')
     18  except NameError:
     19          _ = lambda x:x
     20
     21
     22  _log = logging.getLogger('gm.emr')
     23  _log.info(__version__)
     24
     25
     26  soap_cat2l10n = {
     27          u's': _('soap_S').replace(u'soap_', u''),
     28          u'o': _('soap_O').replace(u'soap_', u''),
     29          u'a': _('soap_A').replace(u'soap_', u''),
     30          u'p': _('soap_P').replace(u'soap_', u''),
     31          u'u': u'?',
     32          None: gmTools.u_ellipsis,
     33          u'': gmTools.u_ellipsis
     34  }
     35
     36  soap_cat2l10n_str = {
     37          u's': _('soap_Subjective').replace(u'soap_', u''),
     38          u'o': _('soap_Objective').replace(u'soap_', u''),
     39          u'a': _('soap_Assessment').replace(u'soap_', u''),
     40          u'p': _('soap_Plan').replace(u'soap_', u''),
     41          u'u': _('soap_Unspecified').replace(u'soap_', u''),
     42          None: _('soap_Administrative').replace(u'soap_', u'')
     43  }
     44
     45  l10n2soap_cat = {
     46          _('soap_S').replace(u'soap_', u''): u's',
     47          _('soap_O').replace(u'soap_', u''): u'o',
     48          _('soap_A').replace(u'soap_', u''): u'a',
     49          _('soap_P').replace(u'soap_', u''): u'p',
     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_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