# Module `Gnumed.business.gmClinNarrative`

GNUmed clinical narrative business object.

## Functions

`def create_narrative_item(narrative=None, soap_cat=None, episode_id=None, encounter_id=None, link_obj=None)`
  Expand source code

      def create_narrative_item(narrative=None, soap_cat=None, episode_id=None, encounter_id=None, link_obj=None):
              """Creates a new clinical narrative entry

                      narrative - free text clinical narrative
                      soap_cat - soap category
                      episode_id - episodes's primary key
                      encounter_id - encounter's primary key

                      any of the args being None (except soap_cat) will fail the SQL code
              """
              # silently do not insert empty narrative
              narrative = narrative.strip()
              if narrative == '':
                      return None

              args = {'enc': encounter_id, 'epi': episode_id, 'soap': soap_cat, 'narr': narrative}

              # insert new narrative
              # but, also silently, do not insert true duplicates
              # this should check for .provider = current_user but
              # the view has provider mapped to their staff alias
              cmd = """
                      INSERT INTO clin.clin_narrative
                              (fk_encounter, fk_episode, narrative, soap_cat)
                      SELECT
                              %(enc)s, %(epi)s, %(narr)s, lower(%(soap)s)
                      WHERE NOT EXISTS (
                              SELECT 1 FROM clin.v_narrative
                              WHERE
                                      pk_encounter = %(enc)s
                                              AND
                                      pk_episode = %(epi)s
                                              AND
                                      soap_cat = lower(%(soap)s)
                                              AND
                                      narrative = %(narr)s
                      )
                      RETURNING pk"""
              rows = gmPG2.run_rw_queries(link_obj = link_obj, queries = [{'sql': cmd, 'args': args}], return_data = True)
              if len(rows) == 1:
                      # re-use same link_obj if given because when called from create_progress_note we won't yet see rows inside a new tx
                      return cNarrative(aPK_obj = rows[0]['pk'], link_obj = link_obj)

              if len(rows) > 1:
                      raise Exception('more than one row returned from single-row INSERT')

              # retrieve existing narrative
              cmd = """
                      SELECT * FROM clin.v_narrative
                      WHERE
                              pk_encounter = %(enc)s
                                      AND
                              pk_episode = %(epi)s
                                      AND
                              soap_cat = lower(%(soap)s)
                                      AND
                              narrative = %(narr)s
              """
              rows = gmPG2.run_ro_queries(link_obj = link_obj, queries = [{'sql': cmd, 'args': args}])
              if len(rows) == 1:
                      return cNarrative(row = {'pk_field': 'pk_narrative', 'data': rows[0]})

              raise Exception('retrieving known-to-exist narrative row returned 0 or >1 result: %s' % len(rows))

  Creates a new clinical narrative entry

  narrative - free text clinical narrative soap_cat - soap category episode_id - episodes's primary key encounter_id - encounter's primary key

  any of the args being None (except soap_cat) will fail the SQL code

`def create_progress_note(soap=None, episode_id=None, encounter_id=None, link_obj=None)`
  Expand source code

      def create_progress_note(soap=None, episode_id=None, encounter_id=None, link_obj=None):
              """Create clinical narrative entries.

              <soap>
                      must be a dict, the keys being SOAP categories (including U and
                      None=admin) and the values being text (possibly multi-line)

              Existing but empty ('' or None) categories are skipped.
              """
              if soap is None:
                      return True

              if not gmSoapDefs.are_valid_soap_cats(list(soap), allow_upper = True):
                      raise ValueError('invalid SOAP category in <soap> dictionary: %s', soap)

              if link_obj is None:
                      link_obj = gmPG2.get_connection(readonly = False)
                      conn_commit = link_obj.commit
                      conn_close = link_obj.close
              else:
                      conn_commit = lambda *x: None
                      conn_close = lambda *x: None

              instances = {}
              for cat in soap:
                      val = soap[cat]
                      if val is None:
                              continue
                      if ''.join([ v.strip() for v in val ]) == '':
                              continue
                      instance = create_narrative_item (
                              narrative = '\n'.join([ v.strip() for v in val ]),
                              soap_cat = cat,
                              episode_id = episode_id,
                              encounter_id = encounter_id,
                              link_obj = link_obj
                      )
                      if instance is None:
                              continue
                      instances[cat] = instance

              conn_commit()
              conn_close()
              return instances

  Create clinical narrative entries.

  must be a dict, the keys being SOAP categories (including U and None=admin) and the values being text (possibly multi-line)

  Existing but empty ('' or None) categories are skipped.

`def delete_clin_narrative(narrative: int = None)`
  Expand source code

      def delete_clin_narrative(narrative:int=None):
              """Deletes a clin.clin_narrative row by it's PK."""
              SQL = 'DELETE FROM clin.clin_narrative WHERE pk = %(pk_narr)s'
              args = {'pk_narr': narrative}
              gmPG2.run_rw_queries(queries = [{'sql': SQL, 'args': args}])
              return True

  Deletes a clin.clin_narrative row by it's PK.

`def format_narrative_for_failsafe_output(pk_patient: int, max_width: int = 80) ‑> list[str]`
  Expand source code

      def format_narrative_for_failsafe_output(pk_patient:int, max_width:int=80) -> list[str]:

              narr_rows = get_as_journal(patient = pk_patient, types = ['clin.clin_narrative'])
              if not narr_rows:
                      return []

              lines = []
              date_len = len(narr_rows[0]['date'])
              empty_date = ' ' * date_len
              empty_soap = ' '
              soap_len = 1
              last_date = None
              last_soap = None
              line_template = '%' + str(date_len) + 's %' + str(soap_len) + 's: %s'
              narr_len = max_width - len(line_template)
              for narr in narr_rows:
                      if not narr['narrative']:
                              continue

                      if last_date == narr['date']:
                              date = empty_date
                      else:
                              date = narr['date']
                              last_date = date
                      if last_soap == narr['soap_cat']:
                              soap = empty_soap
                      else:
                              soap = narr['soap_cat']
                              last_soap = soap

                      wrapped = gmTools.wrap(text = narr['narrative'], width = narr_len)
                      sub_lines = wrapped.split('\n')
                      lines.append(gmTools.shorten_text(line_template % (date, soap, sub_lines[0]), max_width))
                      for sl in sub_lines[1:]:
                              lines.append(gmTools.shorten_text(line_template % (empty_date, empty_soap, sl), max_width))
              return lines

`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,
types=None) ‑> list`
  Expand source code

      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,
              types=None
      ) -> list:

              if (patient is None) and (episodes is None) and (issues is None) and (encounters is None):
                      raise ValueError('at least one of <patient>, <episodes>, <issues>, <encounters> must not be None')

              if order_by is None:
                      order_by = 'ORDER BY clin_when, pk_episode, scr, modified_when, src_table'
              else:
                      order_by = 'ORDER BY %s' % order_by

              where_parts = []
              args = {}
              if patient is not None:
                      where_parts.append('c_vej.pk_patient = %(pat)s')
                      args['pat'] = patient
              if soap_cats is not None:
                      # work around bug in psycopg2 not being able to properly
                      # adapt None to NULL inside tuples
                      if None in soap_cats:
                              where_parts.append('((c_vej.soap_cat = ANY(%(soap_cat)s)) OR (c_vej.soap_cat IS NULL))')
                              soap_cats.remove(None)
                      else:
                              where_parts.append('c_vej.soap_cat = ANY(%(soap_cat)s)')
                      args['soap_cat'] = soap_cats
              if time_range is not None:
                      where_parts.append("c_vej.clin_when > (now() - '%s days'::interval)" % time_range)
              if episodes is not None:
                      where_parts.append("c_vej.pk_episode = ANY(%(epis)s)")
                      args['epis'] = episodes
              if issues is not None:
                      where_parts.append("c_vej.pk_health_issue = ANY(%(issues)s)")
                      args['issues'] = issues
              if types:
                      where_parts.append('c_vej.src_table = ANY(%(src_tbl)s)')
                      args['src_tbl'] = types
              # FIXME: implement more constraints
              cmd_journal = """
                      SELECT
                              to_char(c_vej.clin_when, 'YYYY-MM-DD') AS date,
                              c_vej.clin_when,
                              coalesce(c_vej.soap_cat, '') as soap_cat,
                              c_vej.narrative,
                              c_vej.src_table,
                              c_scr.rank AS scr,
                              c_vej.modified_when,
                              to_char(c_vej.modified_when, 'YYYY-MM-DD HH24:MI') AS date_modified,
                              c_vej.modified_by,
                              c_vej.row_version,
                              c_vej.pk_episode,
                              c_vej.pk_encounter,
                              c_vej.soap_cat as real_soap_cat,
                              c_vej.src_pk,
                              c_vej.pk_health_issue,
                              c_vej.health_issue,
                              c_vej.episode,
                              c_vej.issue_active,
                              c_vej.issue_clinically_relevant,
                              c_vej.episode_open,
                              c_vej.encounter_started,
                              c_vej.encounter_last_affirmed,
                              c_vej.encounter_l10n_type,
                              c_vej.pk_patient
                      FROM
                              clin.v_emr_journal c_vej
                                      join clin.soap_cat_ranks c_scr on (c_scr.soap_cat IS NOT DISTINCT FROM c_vej.soap_cat)
                      WHERE
                              %s
              """ % '\n\t\t\t\t\tAND\n\t\t\t\t'.join(where_parts)

              if active_encounter is None:
                      cmd = cmd_journal + '\n ' + order_by
              else:
                      args['pk_enc'] = active_encounter['pk_encounter']
                      args['enc_start'] = active_encounter['started']
                      args['enc_last_affirmed'] = active_encounter['last_affirmed']
                      args['enc_type'] = active_encounter['l10n_type']
                      args['enc_pat'] = active_encounter['pk_patient']
                      cmd_hints = """
                              SELECT
                                      to_char(now(), 'YYYY-MM-DD') AS date,
                                      now() as clin_when,
                                      'u'::text as soap_cat,
                                      hints.title || E'\n' || hints.hint
                                              as narrative,
                                      -- .src_table does not correspond with the
                                      -- .src_pk column source because it is generated
                                      -- from clin.get_hints_for_patient()
                                      'ref.auto_hint'::text as src_table,
                                      c_scr.rank AS scr,
                                      now() as modified_when,
                                      to_char(now(), 'YYYY-MM-DD HH24:MI') AS date_modified,
                                      current_user as modified_by,
                                      0::integer as row_version,
                                      NULL::integer as pk_episode,
                                      %(pk_enc)s as pk_encounter,
                                      'u'::text as real_soap_cat,
                                      hints.pk_auto_hint as src_pk,
                                      NULL::integer as pk_health_issue,
                                      ''::text as health_issue,
                                      ''::text as episode,
                                      False as issue_active,
                                      False as issue_clinically_relevant,
                                      False as episode_open,
                                      %(enc_start)s as encounter_started,
                                      %(enc_last_affirmed)s  as encounter_last_affirmed,
                                      %(enc_type)s as encounter_l10n_type,
                                      %(enc_pat)s as pk_patient
                              FROM
                                      clin.get_hints_for_patient(%(enc_pat)s) AS hints
                                              JOIN clin.soap_cat_ranks c_scr ON (c_scr.soap_cat = 'u')
                      """
                      cmd = cmd_journal + '\nUNION ALL\n' + cmd_hints + '\n' + order_by

              journal_rows = gmPG2.run_ro_queries(queries = [{'sql': cmd, 'args': args}])
              return journal_rows

`def get_narrative(since=None,
until=None,
encounters=None,
episodes=None,
issues=None,
soap_cats=None,
providers=None,
patient=None,
order_by=None)`
  Expand source code

      def get_narrative(since=None, until=None, encounters=None, episodes=None, issues=None, soap_cats=None, providers=None, patient=None, order_by=None):
              """Get SOAP notes pertinent to this encounter.

                      since
                              - initial date for narrative items
                      until
                              - final date for narrative items
                      encounters
                              - list of encounters whose narrative are to be retrieved
                      episodes
                              - list of episodes whose narrative are to be retrieved
                      issues
                              - list of health issues whose narrative are to be retrieved
                      soap_cats
                              - list of SOAP categories of the narrative to be retrieved
              """
              where_parts = ['TRUE']
              args = {}
              if encounters is not None:
                      where_parts.append('pk_encounter = ANY(%(encs)s)')
                      args['encs'] = encounters
              if episodes is not None:
                      where_parts.append('pk_episode = ANY(%(epis)s)')
                      args['epis'] = episodes
              if issues is not None:
                      where_parts.append('pk_health_issue = ANY(%(issues)s)')
                      args['issues'] = issues
              if patient is not None:
                      where_parts.append('pk_patient = %(pat)s')
                      args['pat'] = patient
              if soap_cats is not None:
                      where_parts.append('c_vn.soap_cat = ANY(%(soap_cats)s)')
                      args['soap_cats'] = soap_cats
              if order_by is None:
                      order_by = 'ORDER BY date, soap_rank'
              else:
                      order_by = 'ORDER BY %s' % order_by
              cmd = """
                      SELECT
                              c_vn.*,
                              c_scr.rank AS soap_rank
                      FROM
                              clin.v_narrative c_vn
                                      LEFT JOIN clin.soap_cat_ranks c_scr ON c_vn.soap_cat = c_scr.soap_cat
                      WHERE
                              %s
                      %s
              """ % (
                      ' AND '.join(where_parts),
                      order_by
              )

              rows = gmPG2.run_ro_queries(queries = [{'sql': cmd, 'args': args}])

              filtered_narrative = [ cNarrative(row = {'pk_field': 'pk_narrative', 'data': row}) for row in rows ]

              if since is not None:
                      filtered_narrative = [ narr for narr in filtered_narrative if narr['date'] >= since ]

              if until is not None:
                      filtered_narrative = [ narr for narr in filtered_narrative if narr['date'] < until ]

              if providers is not None:
                      filtered_narrative = [ narr for narr in filtered_narrative if narr['modified_by'] in providers ]

              return filtered_narrative

  Get SOAP notes pertinent to this encounter.

  since - initial date for narrative items until - final date for narrative items encounters - list of encounters whose narrative are to be retrieved episodes - list of episodes whose narrative are to be retrieved issues - list of health issues whose narrative are to be retrieved soap_cats - list of SOAP categories of the narrative to be retrieved

`def search_text_across_emrs(search_term=None)`
  Expand source code

      def search_text_across_emrs(search_term=None):

              if search_term is None:
                      return []

              if search_term.strip() == '':
                      return []

              cmd = 'SELECT * FROM clin.v_narrative4search WHERE narrative ~* %(term)s ORDER BY pk_patient LIMIT 1000'
              rows = gmPG2.run_ro_queries(queries = [{'sql': cmd, 'args': {'term': search_term}}])
              return rows

## Classes

`class cNarrative (aPK_obj: int | dict = None, row: dict = None, link_obj=None)`
  Expand source code

      class cNarrative(gmBusinessDBObject.cBusinessDBObject):
              """Represents one clinical free text entry."""

              _cmd_fetch_payload = "SELECT * FROM clin.v_narrative WHERE pk_narrative = %s"
              _cmds_store_payload = [
                      """update clin.clin_narrative set
                                      narrative = %(narrative)s,
                                      clin_when = %(date)s,
                                      soap_cat = lower(%(soap_cat)s),
                                      fk_encounter = %(pk_encounter)s,
                                      fk_episode = %(pk_episode)s
                              WHERE
                                      pk = %(pk_narrative)s
                                              AND
                                      xmin = %(xmin_clin_narrative)s
                              RETURNING
                                      xmin AS xmin_clin_narrative"""
                      ]

              _updatable_fields = [
                      'narrative',
                      'date',
                      'soap_cat',
                      'pk_episode',
                      'pk_encounter'
              ]

              #--------------------------------------------------------
              def format_maximum_information(self, patient=None):
                      return self.format(fancy = True, width = 70).split('\n')

              #--------------------------------------------------------
              def format(self, left_margin='', fancy=False, width=75):

                      if fancy:
                              txt = gmTools.wrap (
                                      text = _('%s: %s by %.8s (v%s)\n%s') % (
                                              self._payload['date'].strftime('%x %H:%M'),
                                              gmSoapDefs.soap_cat2l10n_str[self._payload['soap_cat']],
                                              self._payload['modified_by'],
                                              self._payload['row_version'],
                                              self._payload['narrative']
                                      ),
                                      width = width,
                                      initial_indent = '',
                                      subsequent_indent = left_margin + '   '
                              )
                      else:
                              txt = '%s [%s]: %s (%.8s)' % (
                                      self._payload['date'].strftime('%x %H:%M'),
                                      gmSoapDefs.soap_cat2l10n[self._payload['soap_cat']],
                                      self._payload['narrative'],
                                      self._payload['modified_by']
                              )
                              if len(txt) > width:
                                      txt = txt[:width] + gmTools.u_ellipsis

                      return txt

              #--------------------------------------------------------
              def add_code(self, pk_code=None):
                      """<pk_code> must be a value from ref.coding_system_root.pk_coding_system (clin.lnk_code2item_root.fk_generic_code)"""

                      if pk_code in self._payload['pk_generic_codes']:
                              return

                      cmd = """
                              INSERT INTO clin.lnk_code2narrative
                                      (fk_item, fk_generic_code)
                              SELECT
                                      %(item)s,
                                      %(code)s
                              WHERE NOT EXISTS (
                                      SELECT 1 FROM clin.lnk_code2narrative
                                      WHERE
                                              fk_item = %(item)s
                                                      AND
                                              fk_generic_code = %(code)s
                              )"""
                      args = {
                              'item': self._payload['pk_narrative'],
                              'code': pk_code
                      }
                      gmPG2.run_rw_queries(queries = [{'sql': cmd, 'args': args}])
                      return

              #--------------------------------------------------------
              def remove_code(self, pk_code=None):
                      """<pk_code> must be a value from ref.coding_system_root.pk_coding_system (clin.lnk_code2item_root.fk_generic_code)"""
                      cmd = "DELETE FROM clin.lnk_code2narrative WHERE fk_item = %(item)s AND fk_generic_code = %(code)s"
                      args = {
                              'item': self._payload['pk_narrative'],
                              'code': pk_code
                      }
                      gmPG2.run_rw_queries(queries = [{'sql': cmd, 'args': args}])
                      return True

              #--------------------------------------------------------
              # properties
              #--------------------------------------------------------
              def _get_generic_codes(self):
                      if len(self._payload['pk_generic_codes']) == 0:
                              return []

                      cmd = gmCoding._SQL_get_generic_linked_codes % 'pk_generic_code = ANY(%(pks)s)'
                      args = {'pks': self._payload['pk_generic_codes']}
                      rows = gmPG2.run_ro_queries(queries = [{'sql': cmd, 'args': args}])
                      return [ gmCoding.cGenericLinkedCode(row = {'data': r, 'pk_field': 'pk_lnk_code2item'}) for r in rows ]

              def _set_generic_codes(self, pk_codes):
                      queries = []
                      # remove all codes
                      if len(self._payload['pk_generic_codes']) > 0:
                              queries.append ({
                                      'sql': 'DELETE FROM clin.lnk_code2narrative WHERE fk_item = %(narr)s AND fk_generic_code = ANY(%(codes)s)',
                                      'args': {
                                              'narr': self._payload['pk_narrative'],
                                              'codes': self._payload['pk_generic_codes']
                                      }
                              })
                      # add new codes
                      for pk_code in pk_codes:
                              queries.append ({
                                      'sql': 'INSERT INTO clin.lnk_code2narrative (fk_item, fk_generic_code) VALUES (%(narr)s, %(pk_code)s)',
                                      'args': {
                                              'narr': self._payload['pk_narrative'],
                                              'pk_code': pk_code
                                      }
                              })
                      if len(queries) == 0:
                              return
                      # run it all in one transaction
                      gmPG2.run_rw_queries(queries = queries)
                      return

              generic_codes = property(_get_generic_codes, _set_generic_codes)

  Represents one clinical free text entry.

  Call init from child classes like so:

          super().__init__(aPK_obj = aPK_obj, row = row, link_obj = link_obj)

  ## Args

  `aPK_obj`
    retrieve data from backend
  - an scalar value the ._cmd_fetch_payload WHERE condition must be a simple column: "… WHERE pk_col = %s"
  - a dictionary of values the ._cmd_fetch_payload WHERE condition must consume the dictionary and produce a unique row
  `row`
    must hold the fields
  - data: list of column values for the row selected by ._cmd_fetch_payload (as returned by cursor.fetchone() in the DB-API)
  - pk_field: the name of the primary key column OR
  - pk_obj: a dictionary suitable for being passed to cursor.execute and holding the primary key values, used for composite PKs
  - for example:

        row = {
                'data': rows[0],
                'pk_field': 'pk_XXX (the PK column name)',
                'pk_obj': {'pk_col1': pk_col1_val, 'pk_col2': pk_col2_val}
        }

        rows = gmPG2.run_ro_queries(queries = [{'sql': cmd, 'args': args}])
        objects = [ cChildClass(row = {'data': r, 'pk_field': 'the PK column name'}) for r in rows ]

  ### Ancestors

  - cBusinessDBObject

  ### Instance variables

  `prop generic_codes`
    Expand source code

        def _get_generic_codes(self):
                if len(self._payload['pk_generic_codes']) == 0:
                        return []

                cmd = gmCoding._SQL_get_generic_linked_codes % 'pk_generic_code = ANY(%(pks)s)'
                args = {'pks': self._payload['pk_generic_codes']}
                rows = gmPG2.run_ro_queries(queries = [{'sql': cmd, 'args': args}])
                return [ gmCoding.cGenericLinkedCode(row = {'data': r, 'pk_field': 'pk_lnk_code2item'}) for r in rows ]

  ### Methods

  `def add_code(self, pk_code=None)`
    Expand source code

        def add_code(self, pk_code=None):
                """<pk_code> must be a value from ref.coding_system_root.pk_coding_system (clin.lnk_code2item_root.fk_generic_code)"""

                if pk_code in self._payload['pk_generic_codes']:
                        return

                cmd = """
                        INSERT INTO clin.lnk_code2narrative
                                (fk_item, fk_generic_code)
                        SELECT
                                %(item)s,
                                %(code)s
                        WHERE NOT EXISTS (
                                SELECT 1 FROM clin.lnk_code2narrative
                                WHERE
                                        fk_item = %(item)s
                                                AND
                                        fk_generic_code = %(code)s
                        )"""
                args = {
                        'item': self._payload['pk_narrative'],
                        'code': pk_code
                }
                gmPG2.run_rw_queries(queries = [{'sql': cmd, 'args': args}])
                return

    must be a value from ref.coding_system_root.pk_coding_system (clin.lnk_code2item_root.fk_generic_code)

  `def format_maximum_information(self, patient=None)`
    Expand source code

        def format_maximum_information(self, patient=None):
                return self.format(fancy = True, width = 70).split('\n')

  `def remove_code(self, pk_code=None)`
    Expand source code

        def remove_code(self, pk_code=None):
                """<pk_code> must be a value from ref.coding_system_root.pk_coding_system (clin.lnk_code2item_root.fk_generic_code)"""
                cmd = "DELETE FROM clin.lnk_code2narrative WHERE fk_item = %(item)s AND fk_generic_code = %(code)s"
                args = {
                        'item': self._payload['pk_narrative'],
                        'code': pk_code
                }
                gmPG2.run_rw_queries(queries = [{'sql': cmd, 'args': args}])
                return True

    must be a value from ref.coding_system_root.pk_coding_system (clin.lnk_code2item_root.fk_generic_code)

  ### Inherited members

  - `cBusinessDBObject`:
    - `fields_as_dict`
    - `format`
    - `get_fields`
    - `get_patient`
    - `get_updatable_fields`
    - `is_modified`
    - `keys`
    - `patient_pk`
    - `refetch_payload`
    - `same_payload`
    - `save`
    - `save_payload`
    - `staff_id`