# Module `Gnumed.business.gmKeywordExpansion`

GNUmed keyword snippet expansions

Copyright: authors

Expand source code

    # -*- coding: utf-8 -*-
    """GNUmed keyword snippet expansions

    Copyright: authors
    """
    #============================================================
    __author__ = "Karsten Hilbert <Karsten.Hilbert@gmx.net>"
    __license__ = 'GPL v2 or later (details at http://www.gnu.org)'

    import sys
    import os
    import logging

    if __name__ == '__main__':
            sys.path.insert(0, '../../')
            _ = lambda x:x
    from Gnumed.pycommon import gmPG2
    from Gnumed.pycommon import gmBusinessDBObject
    from Gnumed.pycommon import gmTools
    from Gnumed.pycommon import gmMimeLib

    _log = logging.getLogger('gm.kwd_exp')

    #============================================================
    _SQL_get_keyword_expansions = "SELECT * FROM ref.v_your_keyword_expansions WHERE %s"

    class cKeywordExpansion(gmBusinessDBObject.cBusinessDBObject):
            """Keyword indexed text snippets or chunks of data. Used as text macros or to put into documents."""
            _cmd_fetch_payload = _SQL_get_keyword_expansions % "pk_expansion = %s"
            _cmds_store_payload = [
                    """
                            UPDATE ref.keyword_expansion SET
                                    keyword = gm.nullify_empty_string(%(keyword)s),
                                    textual_data = CASE
                                            WHEN gm.nullify_empty_string(%(expansion)s) IS NULL
                                                    THEN CASE
                                                            WHEN binary_data IS NULL THEN '---fake_data---'
                                                            ELSE NULL
                                                    END
                                            ELSE gm.nullify_empty_string(%(expansion)s)
                                    END,
                                    binary_data = CASE
                                            WHEN gm.nullify_empty_string(%(expansion)s) IS NULL THEN binary_data
                                            ELSE NULL
                                    END,
                                    encrypted = %(is_encrypted)s
                            WHERE
                                    pk = %(pk_expansion)s
                                            AND
                                    xmin = %(xmin_expansion)s
                            RETURNING
                                    xmin as xmin_expansion
                    """
            ]
            _updatable_fields = [
                    'keyword',
                    'expansion',
                    'is_encrypted'
            ]

            #--------------------------------------------------------
            def save_to_file(self, aChunkSize=0, target_mime=None, target_extension=None, ignore_conversion_problems=False):

                    if self._payload[self._idx['is_textual']]:
                            return None

                    if self._payload[self._idx['data_size']] == 0:
                            return None

                    exported_fname = gmTools.get_unique_filename(prefix = 'gm-data_snippet-')
                    success = gmPG2.bytea2file (
                            data_query = {
                                    'cmd': 'SELECT substring(binary_data from %(start)s for %(size)s) FROM ref.keyword_expansion WHERE pk = %(pk)s',
                                    'args': {'pk': self.pk_obj}
                            },
                            filename = exported_fname,
                            chunk_size = aChunkSize,
                            data_size = self._payload[self._idx['data_size']]
                    )

                    if not success:
                            return None

                    if target_mime is None:
                            return exported_fname

                    converted_fname = gmMimeLib.convert_file(filename = exported_fname, target_mime = target_mime)
                    if converted_fname is not None:
                            return converted_fname

                    _log.warning('conversion failed')
                    if ignore_conversion_problems:
                            _log.warning('programmed to ignore conversion problems, hoping receiver can handle [%s]', exported_fname)
                            return exported_fname

                    return None

            #--------------------------------------------------------
            def update_data_from_file(self, filename=None):
                    if not (os.access(filename, os.R_OK) and os.path.isfile(filename)):
                            _log.error('[%s] is not a readable file' % filename)
                            return False

                    gmPG2.file2bytea (
                            query = "UPDATE ref.keyword_expansion SET binary_data = %(data)s::bytea, textual_data = NULL WHERE pk = %(pk)s",
                            filename = filename,
                            args = {'pk': self.pk_obj}
                    )

                    # must update XMIN now ...
                    self.refetch_payload()

                    global __textual_expansion_keywords
                    __textual_expansion_keywords = None
                    global __keyword_expansions
                    __keyword_expansions = None

                    return True

            #--------------------------------------------------------
            def format(self):
                    txt = '%s            #%s\n' % (
                            gmTools.bool2subst (
                                    self._payload[self._idx['is_textual']],
                                    _('Textual keyword expansion'),
                                    _('Binary keyword expansion')
                            ),
                            self._payload[self._idx['pk_expansion']]
                    )
                    txt += ' %s%s\n' % (
                            gmTools.bool2subst (
                                    self._payload[self._idx['private_expansion']],
                                    _('private'),
                                    _('public')
                            ),
                            gmTools.bool2subst (
                                    self._payload[self._idx['is_encrypted']],
                                    ', %s' % _('encrypted'),
                                    ''
                            )
                    )
                    txt += _(' Keyword: %s\n') % self._payload[self._idx['keyword']]
                    txt += _(' Owner: %s\n') % self._payload[self._idx['owner']]
                    if self._payload[self._idx['is_textual']]:
                            txt += '\n%s' % self._payload[self._idx['expansion']]
                    else:
                            txt += ' Data: %s (%s Bytes)' % (gmTools.size2str(self._payload[self._idx['data_size']]), self._payload[self._idx['data_size']])

                    return txt

    #------------------------------------------------------------
    __keyword_expansions = None

    def get_keyword_expansions(order_by=None, force_reload=False, return_pks=False):
            global __keyword_expansions
            if not force_reload:
                    if __keyword_expansions is not None:
                            return __keyword_expansions

            if order_by is None:
                    order_by = 'true'
            else:
                    order_by = 'true ORDER BY %s' % order_by

            cmd = _SQL_get_keyword_expansions % order_by
            rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd}], get_col_idx = True)
            if return_pks:
                    return [ r['pk_expansion'] for r in rows ]
            __keyword_expansions = [ cKeywordExpansion(row = {'data': r, 'idx': idx, 'pk_field': 'pk_expansion'}) for r in rows ]
            return __keyword_expansions

    #------------------------------------------------------------
    def get_expansion(keyword=None, textual_only=True, binary_only=False):

            if False not in [textual_only, binary_only]:
                    raise ValueError('one of <textual_only> and <binary_only> must be False')

            where_parts = ['keyword = %(kwd)s']
            args = {'kwd': keyword}

            if textual_only:
                    where_parts.append('is_textual IS TRUE')

            cmd = _SQL_get_keyword_expansions % ' AND '.join(where_parts)
            rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)

            if len(rows) == 0:
                    return None

            return cKeywordExpansion(row = {'data': rows[0], 'idx': idx, 'pk_field': 'pk_expansion'})

    #------------------------------------------------------------
    def create_keyword_expansion(keyword=None, text=None, data_file=None, public=True):

            if text is not None:
                    if text.strip() == '':
                            text = None

            if None not in [text, data_file]:
                    raise ValueError('either <text> or <data_file> must be non-NULL')

            # already exists ?
            cmd = "SELECT 1 FROM ref.v_your_keyword_expansions WHERE public_expansion IS %(public)s AND keyword = %(kwd)s"
            args = {'kwd': keyword, 'public': public}
            rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}])
            if len(rows) != 0:
                    # can't create duplicate
                    return False

            if data_file is not None:
                    text = 'fake data'
            args = {'kwd': keyword, 'txt': text}
            if public:
                    cmd = """
                            INSERT INTO ref.keyword_expansion (keyword, textual_data, fk_staff)
                            VALUES (
                                    gm.nullify_empty_string(%(kwd)s),
                                    gm.nullify_empty_string(%(txt)s),
                                    null
                            )
                            RETURNING pk
                    """
            else:
                    cmd = """
                            INSERT INTO ref.keyword_expansion (keyword, textual_data, fk_staff)
                            VALUES (
                                    gm.nullify_empty_string(%(kwd)s),
                                    gm.nullify_empty_string(%(txt)s),
                                    (SELECT pk FROM dem.staff WHERE db_user = current_user)
                            )
                            RETURNING pk
                    """
            rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}], return_data = True, get_col_idx = False)
            expansion = cKeywordExpansion(aPK_obj = rows[0]['pk'])
            if data_file is not None:
                    expansion.update_data_from_file(filename = data_file)

            global __textual_expansion_keywords
            __textual_expansion_keywords = None
            global __keyword_expansions
            __keyword_expansions = None

            return expansion
    #------------------------------------------------------------
    def delete_keyword_expansion(pk=None):
            args = {'pk': pk}
            cmd = "DELETE FROM ref.keyword_expansion WHERE pk = %(pk)s"
            gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])

            global __textual_expansion_keywords
            __textual_expansion_keywords = None
            global __keyword_expansions
            __keyword_expansions = None

            return True

    #------------------------------------------------------------------------
    #------------------------------------------------------------------------
    #------------------------------------------------------------------------
    #------------------------------------------------------------------------
    __textual_expansion_keywords = None

    def get_textual_expansion_keywords():
            global __textual_expansion_keywords
            if __textual_expansion_keywords is not None:
                    return __textual_expansion_keywords

            cmd = """SELECT keyword, public_expansion, private_expansion, owner FROM ref.v_keyword_expansions WHERE is_textual IS TRUE"""
            rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd}])
            __textual_expansion_keywords = rows

            _log.info('retrieved %s textual expansion keywords', len(__textual_expansion_keywords))

            return __textual_expansion_keywords
    #------------------------------------------------------------------------
    def get_matching_textual_keywords(fragment=None):

            if fragment is None:
                    return []

            return [ kwd['keyword'] for kwd in get_textual_expansion_keywords() if kwd['keyword'].startswith(fragment) ]

    #------------------------------------------------------------------------
    def expand_keyword(keyword = None):

            # Easter Egg ;-)
            if keyword == '$$steffi':
                    return 'Hai, play !  Versucht das ! (Keks dazu ?)  :-)'

            cmd = """SELECT expansion FROM ref.v_your_keyword_expansions WHERE keyword = %(kwd)s"""
            rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': {'kwd': keyword}}])

            if len(rows) == 0:
                    return None

            return rows[0]['expansion']
    #============================================================
    if __name__ == "__main__":

            if len(sys.argv) < 2:
                    sys.exit()

            if sys.argv[1] != 'test':
                    sys.exit()

            logging.basicConfig(level=logging.DEBUG)

            from Gnumed.pycommon import gmI18N
            gmI18N.install_domain('gnumed')
            gmI18N.activate_locale()

            #--------------------------------------------------------------------
            def test_textual_expansion():
                    print("keywords, from database:")
                    print(get_textual_expansion_keywords())
                    print("keywords, cached:")
                    print(get_textual_expansion_keywords())
                    print("'$keyword' expands to:")
                    print(expand_keyword(keyword = '$dvt'))

            #--------------------------------------------------------------------
            def test_kwd_expansions():
                    for k in get_keyword_expansions():
                            print(k.format())
                            print("")
            #--------------------------------------------------------------------
            #test_textual_expansion()
            test_kwd_expansions()

## Functions

`def create_keyword_expansion(keyword=None, text=None, data_file=None, public=True)`
  Expand source code

      def create_keyword_expansion(keyword=None, text=None, data_file=None, public=True):

              if text is not None:
                      if text.strip() == '':
                              text = None

              if None not in [text, data_file]:
                      raise ValueError('either <text> or <data_file> must be non-NULL')

              # already exists ?
              cmd = "SELECT 1 FROM ref.v_your_keyword_expansions WHERE public_expansion IS %(public)s AND keyword = %(kwd)s"
              args = {'kwd': keyword, 'public': public}
              rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}])
              if len(rows) != 0:
                      # can't create duplicate
                      return False

              if data_file is not None:
                      text = 'fake data'
              args = {'kwd': keyword, 'txt': text}
              if public:
                      cmd = """
                              INSERT INTO ref.keyword_expansion (keyword, textual_data, fk_staff)
                              VALUES (
                                      gm.nullify_empty_string(%(kwd)s),
                                      gm.nullify_empty_string(%(txt)s),
                                      null
                              )
                              RETURNING pk
                      """
              else:
                      cmd = """
                              INSERT INTO ref.keyword_expansion (keyword, textual_data, fk_staff)
                              VALUES (
                                      gm.nullify_empty_string(%(kwd)s),
                                      gm.nullify_empty_string(%(txt)s),
                                      (SELECT pk FROM dem.staff WHERE db_user = current_user)
                              )
                              RETURNING pk
                      """
              rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}], return_data = True, get_col_idx = False)
              expansion = cKeywordExpansion(aPK_obj = rows[0]['pk'])
              if data_file is not None:
                      expansion.update_data_from_file(filename = data_file)

              global __textual_expansion_keywords
              __textual_expansion_keywords = None
              global __keyword_expansions
              __keyword_expansions = None

              return expansion

`def delete_keyword_expansion(pk=None)`
  Expand source code

      def delete_keyword_expansion(pk=None):
              args = {'pk': pk}
              cmd = "DELETE FROM ref.keyword_expansion WHERE pk = %(pk)s"
              gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])

              global __textual_expansion_keywords
              __textual_expansion_keywords = None
              global __keyword_expansions
              __keyword_expansions = None

              return True

`def expand_keyword(keyword=None)`
  Expand source code

      def expand_keyword(keyword = None):

              # Easter Egg ;-)
              if keyword == '$$steffi':
                      return 'Hai, play !  Versucht das ! (Keks dazu ?)  :-)'

              cmd = """SELECT expansion FROM ref.v_your_keyword_expansions WHERE keyword = %(kwd)s"""
              rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': {'kwd': keyword}}])

              if len(rows) == 0:
                      return None

              return rows[0]['expansion']

`def get_expansion(keyword=None, textual_only=True, binary_only=False)`
  Expand source code

      def get_expansion(keyword=None, textual_only=True, binary_only=False):

              if False not in [textual_only, binary_only]:
                      raise ValueError('one of <textual_only> and <binary_only> must be False')

              where_parts = ['keyword = %(kwd)s']
              args = {'kwd': keyword}

              if textual_only:
                      where_parts.append('is_textual IS TRUE')

              cmd = _SQL_get_keyword_expansions % ' AND '.join(where_parts)
              rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)

              if len(rows) == 0:
                      return None

              return cKeywordExpansion(row = {'data': rows[0], 'idx': idx, 'pk_field': 'pk_expansion'})

`def get_keyword_expansions(order_by=None, force_reload=False, return_pks=False)`
  Expand source code

      def get_keyword_expansions(order_by=None, force_reload=False, return_pks=False):
              global __keyword_expansions
              if not force_reload:
                      if __keyword_expansions is not None:
                              return __keyword_expansions

              if order_by is None:
                      order_by = 'true'
              else:
                      order_by = 'true ORDER BY %s' % order_by

              cmd = _SQL_get_keyword_expansions % order_by
              rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd}], get_col_idx = True)
              if return_pks:
                      return [ r['pk_expansion'] for r in rows ]
              __keyword_expansions = [ cKeywordExpansion(row = {'data': r, 'idx': idx, 'pk_field': 'pk_expansion'}) for r in rows ]
              return __keyword_expansions

`def get_matching_textual_keywords(fragment=None)`
  Expand source code

      def get_matching_textual_keywords(fragment=None):

              if fragment is None:
                      return []

              return [ kwd['keyword'] for kwd in get_textual_expansion_keywords() if kwd['keyword'].startswith(fragment) ]

`def get_textual_expansion_keywords()`
  Expand source code

      def get_textual_expansion_keywords():
              global __textual_expansion_keywords
              if __textual_expansion_keywords is not None:
                      return __textual_expansion_keywords

              cmd = """SELECT keyword, public_expansion, private_expansion, owner FROM ref.v_keyword_expansions WHERE is_textual IS TRUE"""
              rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd}])
              __textual_expansion_keywords = rows

              _log.info('retrieved %s textual expansion keywords', len(__textual_expansion_keywords))

              return __textual_expansion_keywords

## Classes

`class cKeywordExpansion (aPK_obj=None, row: dict = None, link_obj=None)`

  Keyword indexed text snippets or chunks of data. Used as text macros or to put into documents.

  Init business object.

  Call from child classes like so:

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

  ## Args

  `aPK_obj`
    retrieve data from backend * a simple value the primary key WHERE condition must be a simple column * a dictionary of values the primary key WHERE condition must be a subselect consuming the dict and producing the single-value primary key
  `row`
    must hold the fields * idx: a dict mapping field names to position * data: the field values in a list (as returned by cursor.fetchone() in the DB-API) * pk_field: the name of the primary key field OR * pk_obj: a dictionary suitable for passed to cursor.execute and holding the primary key values, used for composite PKs

  ## Examples

  row = { 'data': rows[0], 'idx': idx, 'pk_field': 'pk_XXX (the PK column name)', 'pk_obj': {'pk_col1': pk_col1_val, 'pk_col2': pk_col2_val} } rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True) objects = [ cChildClass(row = {'data': r, 'idx': idx, 'pk_field': 'the PK column name'}) for r in rows ]

  Expand source code

      class cKeywordExpansion(gmBusinessDBObject.cBusinessDBObject):
              """Keyword indexed text snippets or chunks of data. Used as text macros or to put into documents."""
              _cmd_fetch_payload = _SQL_get_keyword_expansions % "pk_expansion = %s"
              _cmds_store_payload = [
                      """
                              UPDATE ref.keyword_expansion SET
                                      keyword = gm.nullify_empty_string(%(keyword)s),
                                      textual_data = CASE
                                              WHEN gm.nullify_empty_string(%(expansion)s) IS NULL
                                                      THEN CASE
                                                              WHEN binary_data IS NULL THEN '---fake_data---'
                                                              ELSE NULL
                                                      END
                                              ELSE gm.nullify_empty_string(%(expansion)s)
                                      END,
                                      binary_data = CASE
                                              WHEN gm.nullify_empty_string(%(expansion)s) IS NULL THEN binary_data
                                              ELSE NULL
                                      END,
                                      encrypted = %(is_encrypted)s
                              WHERE
                                      pk = %(pk_expansion)s
                                              AND
                                      xmin = %(xmin_expansion)s
                              RETURNING
                                      xmin as xmin_expansion
                      """
              ]
              _updatable_fields = [
                      'keyword',
                      'expansion',
                      'is_encrypted'
              ]

              #--------------------------------------------------------
              def save_to_file(self, aChunkSize=0, target_mime=None, target_extension=None, ignore_conversion_problems=False):

                      if self._payload[self._idx['is_textual']]:
                              return None

                      if self._payload[self._idx['data_size']] == 0:
                              return None

                      exported_fname = gmTools.get_unique_filename(prefix = 'gm-data_snippet-')
                      success = gmPG2.bytea2file (
                              data_query = {
                                      'cmd': 'SELECT substring(binary_data from %(start)s for %(size)s) FROM ref.keyword_expansion WHERE pk = %(pk)s',
                                      'args': {'pk': self.pk_obj}
                              },
                              filename = exported_fname,
                              chunk_size = aChunkSize,
                              data_size = self._payload[self._idx['data_size']]
                      )

                      if not success:
                              return None

                      if target_mime is None:
                              return exported_fname

                      converted_fname = gmMimeLib.convert_file(filename = exported_fname, target_mime = target_mime)
                      if converted_fname is not None:
                              return converted_fname

                      _log.warning('conversion failed')
                      if ignore_conversion_problems:
                              _log.warning('programmed to ignore conversion problems, hoping receiver can handle [%s]', exported_fname)
                              return exported_fname

                      return None

              #--------------------------------------------------------
              def update_data_from_file(self, filename=None):
                      if not (os.access(filename, os.R_OK) and os.path.isfile(filename)):
                              _log.error('[%s] is not a readable file' % filename)
                              return False

                      gmPG2.file2bytea (
                              query = "UPDATE ref.keyword_expansion SET binary_data = %(data)s::bytea, textual_data = NULL WHERE pk = %(pk)s",
                              filename = filename,
                              args = {'pk': self.pk_obj}
                      )

                      # must update XMIN now ...
                      self.refetch_payload()

                      global __textual_expansion_keywords
                      __textual_expansion_keywords = None
                      global __keyword_expansions
                      __keyword_expansions = None

                      return True

              #--------------------------------------------------------
              def format(self):
                      txt = '%s            #%s\n' % (
                              gmTools.bool2subst (
                                      self._payload[self._idx['is_textual']],
                                      _('Textual keyword expansion'),
                                      _('Binary keyword expansion')
                              ),
                              self._payload[self._idx['pk_expansion']]
                      )
                      txt += ' %s%s\n' % (
                              gmTools.bool2subst (
                                      self._payload[self._idx['private_expansion']],
                                      _('private'),
                                      _('public')
                              ),
                              gmTools.bool2subst (
                                      self._payload[self._idx['is_encrypted']],
                                      ', %s' % _('encrypted'),
                                      ''
                              )
                      )
                      txt += _(' Keyword: %s\n') % self._payload[self._idx['keyword']]
                      txt += _(' Owner: %s\n') % self._payload[self._idx['owner']]
                      if self._payload[self._idx['is_textual']]:
                              txt += '\n%s' % self._payload[self._idx['expansion']]
                      else:
                              txt += ' Data: %s (%s Bytes)' % (gmTools.size2str(self._payload[self._idx['data_size']]), self._payload[self._idx['data_size']])

                      return txt

  ### Ancestors

  - cBusinessDBObject

  ### Methods

  `def save_to_file(self, aChunkSize=0, target_mime=None, target_extension=None, ignore_conversion_problems=False)`
    Expand source code

        def save_to_file(self, aChunkSize=0, target_mime=None, target_extension=None, ignore_conversion_problems=False):

                if self._payload[self._idx['is_textual']]:
                        return None

                if self._payload[self._idx['data_size']] == 0:
                        return None

                exported_fname = gmTools.get_unique_filename(prefix = 'gm-data_snippet-')
                success = gmPG2.bytea2file (
                        data_query = {
                                'cmd': 'SELECT substring(binary_data from %(start)s for %(size)s) FROM ref.keyword_expansion WHERE pk = %(pk)s',
                                'args': {'pk': self.pk_obj}
                        },
                        filename = exported_fname,
                        chunk_size = aChunkSize,
                        data_size = self._payload[self._idx['data_size']]
                )

                if not success:
                        return None

                if target_mime is None:
                        return exported_fname

                converted_fname = gmMimeLib.convert_file(filename = exported_fname, target_mime = target_mime)
                if converted_fname is not None:
                        return converted_fname

                _log.warning('conversion failed')
                if ignore_conversion_problems:
                        _log.warning('programmed to ignore conversion problems, hoping receiver can handle [%s]', exported_fname)
                        return exported_fname

                return None

  `def update_data_from_file(self, filename=None)`
    Expand source code

        def update_data_from_file(self, filename=None):
                if not (os.access(filename, os.R_OK) and os.path.isfile(filename)):
                        _log.error('[%s] is not a readable file' % filename)
                        return False

                gmPG2.file2bytea (
                        query = "UPDATE ref.keyword_expansion SET binary_data = %(data)s::bytea, textual_data = NULL WHERE pk = %(pk)s",
                        filename = filename,
                        args = {'pk': self.pk_obj}
                )

                # must update XMIN now ...
                self.refetch_payload()

                global __textual_expansion_keywords
                __textual_expansion_keywords = None
                global __keyword_expansions
                __keyword_expansions = None

                return True

  ### Inherited members

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