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
      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