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

# Source Code for Module Gnumed.business.gmExportArea

       1  """GNUmed export area
       2
       3  Think shopping cart in a web shop.
       4
       5  This is where you want to put documents for further
       6  processing by you or someone else, like your secretary.
       7  """
       8  #============================================================
       9  __license__ = "GPL v2 or later"
      10  __author__ = "K.Hilbert <Karsten.Hilbert@gmx.net>"
      11
      12
      13  import sys
      14  import logging
      15  import shutil
      16  import os
      17  import io
      18  import platform
      19
      20
      21  if __name__ == '__main__':
      22          sys.path.insert(0, '../../')
      23          from Gnumed.pycommon import gmI18N
      24          gmI18N.activate_locale()
      25          gmI18N.install_domain()
      26  from Gnumed.pycommon import gmTools
      27  from Gnumed.pycommon import gmBusinessDBObject
      28  from Gnumed.pycommon import gmPG2
      29  from Gnumed.pycommon import gmMimeLib
      30  from Gnumed.pycommon import gmDateTime
      31  from Gnumed.pycommon import gmCfg2
      32  from Gnumed.pycommon import gmCrypto
      33
      34  from Gnumed.business import gmDocuments
      35  from Gnumed.business import gmKeywordExpansion
      36
      37
      38  _log = logging.getLogger('gm.exp_area')
      39  _cfg = gmCfg2.gmCfgData()
      40
      41  PRINT_JOB_DESIGNATION = 'print'
      42  DOCUMENTS_SUBDIR = 'documents'
      43  DIRENTRY_README_NAME = '.README.GNUmed-DIRENTRY'
      44
      45  #============================================================
      46  # export area item handling
      47  #------------------------------------------------------------
      48  _SQL_get_export_items = "SELECT * FROM clin.v_export_items WHERE %s"
      49
      50 -class cExportItem(gmBusinessDBObject.cBusinessDBObject):
      51          """Represents an item in the export area table"""
      52
      53          _cmd_fetch_payload = _SQL_get_export_items % "pk_export_item = %s"
      54          _cmds_store_payload = [
      55                  """UPDATE clin.export_item SET
      56                                  fk_identity = %(pk_identity)s,
      57                                  created_by = gm.nullify_empty_string(%(created_by)s),
      58                                  created_when = %(created_when)s,
      59                                  designation = gm.nullify_empty_string(%(designation)s),
      60                                  description = gm.nullify_empty_string(%(description)s),
      61                                  fk_doc_obj = %(pk_doc_obj)s,
      62                                  data = CASE
      63                                          WHEN %(pk_doc_obj)s IS NULL THEN coalesce(data, 'to be replaced by real data')
      64                                          ELSE NULL
      65                                  END,
      66                                  filename = CASE
      67                                          WHEN %(pk_doc_obj)s IS NULL THEN gm.nullify_empty_string(%(filename)s)
      68                                          ELSE NULL
      69                                  END
      70                          WHERE
      71                                  pk = %(pk_export_item)s
      72                                          AND
      73                                  xmin = %(xmin_export_item)s
      74                  """,
      75                  _SQL_get_export_items % 'pk_export_item = %(pk_export_item)s'
      76          ]
      77          _updatable_fields = [
      78                  'pk_identity',
      79                  'created_when',
      80                  'designation',
      81                  'description',
      82                  'pk_doc_obj',
      83                  'filename'
      84          ]
      85          #--------------------------------------------------------
      86 -        def __init__(self, aPK_obj=None, row=None, link_obj=None):
      87                  super(cExportItem, self).__init__(aPK_obj = aPK_obj, row = row, link_obj = link_obj)
      88                  # force auto-healing if need be
      89                  if self._payload[self._idx['pk_identity_raw_needs_update']]:
      90                          _log.warning (
      91                                  'auto-healing export item [%s] from identity [%s] to [%s] because of document part [%s] seems necessary',
      92                                  self._payload[self._idx['pk_export_item']],
      93                                  self._payload[self._idx['pk_identity_raw']],
      94                                  self._payload[self._idx['pk_identity']],
      95                                  self._payload[self._idx['pk_doc_obj']]
      96                          )
      97                          if self._payload[self._idx['pk_doc_obj']] is None:
      98                                  _log.error('however, .fk_doc_obj is NULL, which should not happen, leaving things alone for manual inspection')
      99                                  return
     100                          # only flag ourselves as modified, do not actually
     101                          # modify any values, better safe than sorry
     102                          self._is_modified = True
     103                          self.save()
     104                          self.refetch_payload(ignore_changes = False, link_obj = link_obj)
     105
     106          #--------------------------------------------------------
     107  #       def format(self):
     108  #               return u'%s' % self
     109          #--------------------------------------------------------
     110 -        def update_data(self, data=None):
     111                  assert (data is not None), '<data> must not be <None>'
     112
     113                  SQL = """
     114                          UPDATE clin.export_item SET
     115                                  data = %(data)s::bytea,
     116                                  fk_doc_obj = NULL
     117                          WHERE pk = %(pk)s"""
     118                  args = {'pk': self.pk_obj, 'data': data}
     119                  gmPG2.run_rw_queries(queries = [{'cmd': SQL, 'args': args}], return_data = False, get_col_idx = False)
     120                  # must update XMIN now ...
     121                  self.refetch_payload()
     122                  return True
     123
     124          #--------------------------------------------------------
     125 -        def update_data_from_file(self, filename=None):
     126                  # sanity check
     127                  if not (os.access(filename, os.R_OK) and os.path.isfile(filename)):
     128                          _log.error('[%s] is not a readable file' % filename)
     129                          return False
     130
     131                  cmd = """
     132                          UPDATE clin.export_item SET
     133                                  data = %(data)s::bytea,
     134                                  fk_doc_obj = NULL,
     135                                  filename = gm.nullify_empty_string(%(fname)s)
     136                          WHERE pk = %(pk)s"""
     137                  args = {'pk': self.pk_obj, 'fname': filename}
     138                  if not gmPG2.file2bytea(query = cmd, filename = filename, args = args):
     139                          return False
     140
     141                  # must update XMIN now ...
     142                  self.refetch_payload()
     143                  return True
     144
     145          #--------------------------------------------------------
     146 -        def save_to_file(self, aChunkSize=0, filename=None, directory=None, passphrase=None):
     147
     148                  # data linked from archive ?
     149                  part_fname = self.__save_doc_obj(filename = filename, directory = directory, passphrase = passphrase)
     150                  if part_fname is False:
     151                          return None
     152                  if part_fname is not None:
     153                          return part_fname
     154
     155                  # valid DIRENTRY ?
     156                  if self.is_valid_DIRENTRY:
     157                          target_dir = self.__save_direntry(directory, passphrase = passphrase)
     158                          if target_dir is False:
     159                                  return None
     160                          if target_dir is not None:
     161                                  return target_dir
     162                  # but still a DIRENTRY ?
     163                  if self.is_DIRENTRY:
     164                          # yes, but not valid (other machine, local dir not found)
     165                          return None
     166
     167                  # normal item with data in export area table
     168                  if filename is None:
     169                          filename = self.get_useful_filename(directory = directory)
     170                  success = gmPG2.bytea2file (
     171                          data_query = {
     172                                  'cmd': 'SELECT substring(data from %(start)s for %(size)s) FROM clin.export_item WHERE pk = %(pk)s',
     173                                  'args': {'pk': self.pk_obj}
     174                          },
     175                          filename = filename,
     176                          chunk_size = aChunkSize,
     177                          data_size = self._payload[self._idx['size']]
     178                  )
     179                  if not success:
     180                          return None
     181
     182                  if filename.endswith('.dat'):
     183                          filename = gmMimeLib.adjust_extension_by_mimetype(filename)
     184                  if passphrase is None:
     185                          return filename
     186
     187                  enc_filename = gmCrypto.encrypt_file (
     188                          filename = filename,
     189                          passphrase = passphrase,
     190                          verbose = _cfg.get(option = 'debug'),
     191                          remove_unencrypted = True
     192                  )
     193                  removed = gmTools.remove_file(filename)
     194                  if enc_filename is None:
     195                          _log.error('cannot encrypt')
     196                          return None
     197                  if removed:
     198                          return enc_filename
     199                  _log.error('cannot remove unencrypted file')
     200                  gmTools.remove(enc_filename)
     201                  return None
     202
     203          #--------------------------------------------------------
     204 -        def display_via_mime(self, chunksize=0, block=None):
     205
     206                  # document part
     207                  if self._payload[self._idx['pk_doc_obj']] is not None:
     208                          return self.document_part.display_via_mime(chunksize = chunksize, block = block)
     209
     210                  # DIR entry
     211                  if self._payload[self._idx['filename']].startswith('DIR::'):
     212                          # FIXME: error handling with malformed entries
     213                          tag, node, path = self._payload[self._idx['filename']].split('::', 2)
     214                          if node != platform.node():
     215                                  msg = _(
     216                                          'This item points to a directory on the computer named:\n'
     217                                          ' %s\n'
     218                                          'You are, however, currently using another computer:\n'
     219                                          ' %s\n'
     220                                          'Directory items can only be viewed/saved/exported\n'
     221                                          'on the computer they are pointing to.'
     222                                  ) % (node, platform.node())
     223                                  return False, msg
     224                          success, msg = gmMimeLib.call_viewer_on_file(path, block = block)
     225                          return success, msg
     226
     227                  # data -> save
     228                  fname = self.save_to_file(aChunkSize = chunksize)
     229                  if fname is None:
     230                          return False, ''
     231
     232                  success, msg = gmMimeLib.call_viewer_on_file(fname, block = block)
     233                  if not success:
     234                          return False, msg
     235
     236                  return True, ''
     237
     238          #--------------------------------------------------------
     239 -        def get_useful_filename(self, patient=None, directory=None):
     240                  patient_part = ''
     241                  if patient is not None:
     242                          patient_part = '-%s' % patient.subdir_name
     243
     244                  # preserve original filename extension if available
     245                  suffix = '.dat'
     246                  if self._payload[self._idx['filename']] is not None:
     247                          tmp, suffix = os.path.splitext (
     248                                  gmTools.fname_sanitize(self._payload[self._idx['filename']]).lower()
     249                          )
     250                          if suffix == '':
     251                                  suffix = '.dat'
     252                  fname = gmTools.get_unique_filename (
     253                          prefix = 'gm-export_item%s-' % patient_part,
     254                          suffix = suffix,
     255                          tmp_dir = directory
     256                  )
     257                  return fname
     258
     259          #--------------------------------------------------------
     260          # helpers
     261          #--------------------------------------------------------
     262 -        def __save_doc_obj(self, filename=None, directory=None, passphrase=None):
     263                  """Save doc object part into target.
     264
     265                  None: not a doc obj
     266                  True: success
     267                  False: failure
     268                  """
     269                  if self._payload[self._idx['pk_doc_obj']] is None:
     270                          return None
     271
     272                  part = self.document_part
     273                  if filename is None:
     274                          filename = part.get_useful_filename (
     275                                  make_unique = False,
     276                                  directory = directory,
     277                                  include_gnumed_tag = False,
     278                                  date_before_type = True,
     279                                  name_first = False
     280                          )
     281                  part_fname = part.save_to_file (
     282                          aChunkSize = aChunkSize,
     283                          filename = filename,
     284                          ignore_conversion_problems = True,
     285                          adjust_extension = True
     286                  )
     287                  if part_fname is None:
     288                          _log.error('cannot save document part to file')
     289                          return False
     290
     291                  if passphrase is None:
     292                          return part_fname
     293
     294                  enc_filename = gmCrypto.encrypt_file (
     295                          filename = part_fname,
     296                          passphrase = passphrase,
     297                          verbose = _cfg.get(option = 'debug'),
     298                          remove_unencrypted = True
     299                  )
     300                  removed = gmTools.remove_file(filename)
     301                  if enc_filename is None:
     302                          _log.error('cannot encrypt')
     303                          return False
     304                  if removed:
     305                          return enc_filename
     306                  _log.error('cannot remove unencrypted file')
     307                  gmTools.remove(enc_filename)
     308                  return False
     309
     310          #--------------------------------------------------------
     311 -        def __save_direntry(self, directory=None, passphrase=None):
     312                  """Move DIRENTRY source into target.
     313
     314                  None: not a DIRENTRY
     315                  True: success
     316                  False: failure
     317                  """
     318                  # do not process malformed entries
     319                  try:
     320                          tag, node, local_fs_path = self._payload[self._idx['filename']].split('::', 2)
     321                  except ValueError:
     322                          _log.exception('malformed DIRENTRY: [%s]', self._payload[self._idx['filename']])
     323                          return False
     324                  # source and target paths must not overlap
     325                  if directory is None:
     326                          directory = gmTools.mk_sandbox_dir(prefix = 'exp-')
     327                  if directory.startswith(local_fs_path):
     328                          _log.error('cannot dump DIRENTRY item [%s]: must not be subdirectory of target dir [%s]', self._payload[self._idx['filename']], directory)
     329                          return False
     330                  if local_fs_path.startswith(directory):
     331                          _log.error('cannot dump DIRENTRY item [%s]: target dir [%s] must not be subdirectory of DIRENTRY', self._payload[self._idx['filename']], directory)
     332                          return False
     333
     334                  _log.debug('dumping DIRENTRY item [%s] into [%s]', self._payload[self._idx['filename']], directory)
     335                  sandbox_dir = gmTools.mk_sandbox_dir()
     336                  _log.debug('sandbox: %s', sandbox_dir)
     337                  tmp = gmTools.copy_tree_content(local_fs_path, sandbox_dir)
     338                  if tmp is None:
     339                          _log.error('cannot dump DIRENTRY item [%s] into [%s]: copy error', self._payload[self._idx['filename']], sandbox_dir)
     340                          return False
     341
     342                  gmTools.remove_file(os.path.join(tmp, DIRENTRY_README_NAME))
     343
     344                  if passphrase is not None:
     345                          _log.debug('encrypting sandbox: %s', sandbox_dir)
     346                          encrypted = gmCrypto.encrypt_directory_content (
     347                                  directory = sandbox_dir,
     348                                  passphrase = passphrase,
     349                                  verbose = _cfg.get(option = 'debug'),
     350                                  remove_unencrypted = True
     351                          )
     352                          if not encrypted:
     353                                  _log.error('cannot dump DIRENTRY item [%s]: encryption problem in [%s]', self._payload[self._idx['filename']], sandbox_dir)
     354                                  return False
     355
     356                  tmp = gmTools.copy_tree_content(sandbox_dir, directory)
     357                  if tmp is None:
     358                          _log.debug('cannot dump DIRENTRY item [%s] into [%s]: copy error', self._payload[self._idx['filename']], directory)
     359                          return False
     360
     361                  return directory
     362
     363          #--------------------------------------------------------
     364          # properties
     365          #--------------------------------------------------------
     366 -        def _get_doc_part(self):
     367                  if self._payload[self._idx['pk_doc_obj']] is None:
     368                          return None
     369                  return gmDocuments.cDocumentPart(aPK_obj = self._payload[self._idx['pk_doc_obj']])
     370
     371          document_part = property(_get_doc_part, lambda x:x)
     372
     373          #--------------------------------------------------------
     374 -        def _get_is_print_job(self):
     375                  return self._payload[self._idx['designation']] == PRINT_JOB_DESIGNATION
     376
     377 -        def _set_is_print_job(self, is_print_job):
     378                  desig = gmTools.bool2subst(is_print_job, PRINT_JOB_DESIGNATION, None, None)
     379                  if self._payload[self._idx['designation']] == desig:
     380                          return
     381                  self['designation'] = desig
     382                  self.save()
     383
     384          is_print_job = property(_get_is_print_job, _set_is_print_job)
     385
     386          #--------------------------------------------------------
     387 -        def _is_DIRENTRY(self):
     388                  """Check whether this item looks like a DIRENTRY."""
     389                  if self._payload[self._idx['filename']] is None:
     390                          return False
     391                  if not self._payload[self._idx['filename']].startswith('DIR::'):
     392                          return False
     393                  if len(self._payload[self._idx['filename']].split('::', 2)) != 3:
     394                          return False
     395                  return True
     396
     397          is_DIRENTRY = property(_is_DIRENTRY)
     398
     399          #--------------------------------------------------------
     400 -        def _is_valid_DIRENTRY(self):
     401                  """Check whether this item is a _valid_ DIRENTRY."""
     402                  if not self.is_DIRENTRY:
     403                          return False
     404                  # malformed ?
     405                  try:
     406                          tag, node, local_fs_path = self._payload[self._idx['filename']].split('::', 2)
     407                  except ValueError:
     408                          # should not happen because structure already checked in .is_DIRENTRY,
     409                          # better safe than sorry
     410                          _log.exception('DIRENTRY [%s]: malformed', self._payload[self._idx['filename']])
     411                          return False
     412                  # this machine ?
     413                  if node != platform.node():
     414                          _log.warning('DIRENTRY [%s]: not on this machine (%s)', self._payload[self._idx['filename']], platform.node())
     415                          return False
     416                  # valid path ?
     417                  if not os.path.isdir(local_fs_path):
     418                          _log.warning('DIRENTRY [%s]: directory not found (old DIRENTRY ?)', self._payload[self._idx['filename']])
     419                          return False
     420                  return True
     421
     422          is_valid_DIRENTRY = property(_is_valid_DIRENTRY)
     423
     424          #--------------------------------------------------------
     425 -        def _is_DICOM_directory(self):
     426                  """Check whether this item points to a DICOMDIR."""
     427                  if not self._is_valid_DIRENTRY:
     428                          return False
     429                  tag, node, local_fs_path = self._payload[self._idx['filename']].split('::', 2)
     430                  found_DICOMDIR = False
     431                  for fs_entry in os.listdir(local_fs_path):
     432                          # found a subdir
     433                          if os.path.isdir(os.path.join(local_fs_path, fs_entry)):
     434                                  # allow for any number of subdirs
     435                                  continue
     436                          # found a file
     437                          if fs_entry != 'DICOMDIR':
     438                                  # not named "DICOMDIR" -> not a DICOMDIR DIRENTRY
     439                                  return False
     440                          # must be named DICOMDIR -> that's the only file allowed (and required) in ./
     441                          found_DICOMDIR = True
     442                  return found_DICOMDIR
     443
     444          is_DICOM_directory = property(_is_DICOM_directory)
     445
     446          #--------------------------------------------------------
     447 -        def _has_files_in_root(self):
     448                  """True if there are files in the root directory."""
     449                  tag, node, local_fs_path = self._payload[self._idx['filename']].split('::', 2)
     450                  for fs_entry in os.listdir(local_fs_path):
     451                          if os.path.isfile(fs_entry):
     452                                  _log.debug('has files in top level: %s', local_fs_path)
     453                                  return True
     454                  return False
     455
     456          has_files_in_root = property(_has_files_in_root)
     457
     458  #------------------------------------------------------------
     459 -def get_export_items(order_by=None, pk_identity=None, designation=None, return_pks=False):
     460
     461          args = {
     462                  'pat': pk_identity,
     463                  'desig': gmTools.coalesce(designation, PRINT_JOB_DESIGNATION)
     464          }
     465          where_parts = []
     466          if pk_identity is not None:
     467                  where_parts.append('pk_identity = %(pat)s')
     468                  # note that invalidly linked items will be
     469                  # auto-healed when instantiated
     470          if designation is None:
     471                  where_parts.append("designation IS DISTINCT FROM %(desig)s")
     472          else:
     473                  where_parts.append('designation = %(desig)s')
     474
     475          if order_by is None:
     476                  order_by = ''
     477          else:
     478                  order_by = ' ORDER BY %s' % order_by
     479
     480          cmd = (_SQL_get_export_items % ' AND '.join(where_parts)) + order_by
     481          rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
     482          if return_pks:
     483                  return [ r['pk_export_item'] for r in rows ]
     484          return [ cExportItem(row = {'data': r, 'idx': idx, 'pk_field': 'pk_export_item'}) for r in rows ]
     485
     486  #------------------------------------------------------------
     487 -def get_print_jobs(order_by=None, pk_identity=None):
     488          return get_export_items(order_by = order_by, pk_identity = pk_identity, designation = PRINT_JOB_DESIGNATION)
     489
     490  #------------------------------------------------------------
     491 -def create_export_item(description=None, pk_identity=None, pk_doc_obj=None, filename=None):
     492
     493          args = {
     494                  'desc': description,
     495                  'pk_obj': pk_doc_obj,
     496                  'pk_pat': pk_identity,
     497                  'fname': filename
     498          }
     499          cmd = """
     500                  INSERT INTO clin.export_item (
     501                          description,
     502                          fk_doc_obj,
     503                          fk_identity,
     504                          data,
     505                          filename
     506                  ) VALUES (
     507                          gm.nullify_empty_string(%(desc)s),
     508                          %(pk_obj)s,
     509                          %(pk_pat)s,
     510                          (CASE
     511                                  WHEN %(pk_obj)s IS NULL THEN %(fname)s::bytea
     512                                  ELSE NULL::bytea
     513                          END),
     514                          (CASE
     515                                  WHEN %(pk_obj)s IS NULL THEN gm.nullify_empty_string(%(fname)s)
     516                                  ELSE NULL
     517                          END)
     518                  )
     519                  RETURNING pk
     520          """
     521          rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}], return_data = True, get_col_idx = False)
     522
     523          return cExportItem(aPK_obj = rows[0]['pk'])
     524
     525  #------------------------------------------------------------
     526 -def delete_export_item(pk_export_item=None):
     527          args = {'pk': pk_export_item}
     528          cmd = "DELETE FROM clin.export_item WHERE pk = %(pk)s"
     529          gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
     530          return True
     531
     532  #============================================================
     533  _FRONTPAGE_HTML_CONTENT = """<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
     534         "http://www.w3.org/TR/html4/loose.dtd">
     535  <html>
     536  <head>
     537  <meta http-equiv="content-type" content="text/html; charset=UTF-8">
     538  <link rel="icon" type="image/x-icon" href="gnumed.ico">
     539  <title>%(html_title_header)s %(html_title_patient)s</title>
     540  </head>
     541  <body>
     542
     543  <h1>%(title)s</h1>
     544
     545  <p>
     546  (%(date)s)<br>
     547  </p>
     548
     549  <h2><a href="patient.vcf">Patient</a></h2>
     550
     551  <p>
     552          %(pat_name)s<br>
     553          %(pat_dob)s
     554  </p>
     555
     556  <p><img src="%(mugshot_url)s" alt="%(mugshot_alt)s" title="%(mugshot_title)s" width="200" border="2"></p>
     557
     558  <h2>%(docs_title)s</h2>
     559
     560  <ul>
     561          <li><a href="./">%(browse_root)s</a></li>
     562          <li><a href="%(doc_subdir)s/">%(browse_docs)s</a></li>
     563          %(browse_dicomdir)s
     564          %(run_dicom_viewer)s
     565  </ul>
     566
     567  <ul>
     568  %(docs_list)s
     569  </ul>
     570
     571  <h2><a href="praxis.vcf">Praxis</a></h2>
     572
     573  <p>
     574  %(branch)s @ %(praxis)s
     575  %(adr)s
     576  </p>
     577
     578  <p>(<a href="http://www.gnumed.de">GNUmed</a> version %(gm_ver)s)</p>
     579
     580  </body>
     581  </html>
     582  """
     583
     584  _INDEX_HTML_CONTENT = """<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
     585         "http://www.w3.org/TR/html4/loose.dtd">
     586  <html>
     587  <head>
     588  <meta http-equiv="content-type" content="text/html; charset=UTF-8">
     589  <link rel="icon" type="image/x-icon" href="gnumed.ico">
     590  <title>%(html_title_header)s</title>
     591  </head>
     592  <body>
     593
     594  <h1>%(title)s</h1>
     595
     596  <p>
     597  (%(date)s)<br>
     598  </p>
     599
     600  This is an encrypted patient data excerpt created by the GNUmed Electronic Medical Record.
     601
     602  <p>
     603  For decryption you will need to
     604
     605  <ul>
     606          <li>install decryption software and</li>
     607          <li>obtain relevant passwords from the creator or holder of this media</li>
     608  </ul>
     609
     610  <h2>Decryption software</h2>
     611
     612  For files ending in
     613
     614  <ul>
     615          <li>.asc: install <a href="https://gnupg.org">GNU Privacy Guard</a></li>
     616          <li>.7z: install <a href="https://www.7-zip.org">7-zip</a> or <a href="https://www.winzip.com">WinZip</a></li>
     617  </ul>
     618
     619
     620  <h2>%(docs_title)s</h2>
     621
     622  <ul>
     623          <li><a href="./frontpage.html">front page (after decryption)</a></li>
     624          <li><a href="./%(frontpage_fname)s">front page (if decryption from browser is supported)</a></li>
     625
     626          <li><a href="./">%(browse_root)s</a></li>
     627          <li><a href="%(doc_subdir)s/">%(browse_docs)s</a></li>
     628  </ul>
     629
     630
     631  <h2><a href="praxis.vcf">Praxis</a></h2>
     632
     633  <p>
     634  %(branch)s @ %(praxis)s
     635  %(adr)s
     636  </p>
     637
     638  <p>(<a href="http://www.gnumed.de">GNUmed</a> version %(gm_ver)s)</p>
     639
     640  </body>
     641  </html>
     642  """
     643
     644  #------------------------------------------------------------
     645 -class cExportArea(object):
     646
     647 -        def __init__(self, pk_identity):
     648                  self.__pk_identity = pk_identity
     649
     650          #--------------------------------------------------------
     651 -        def add_form(self, form=None, designation=None):
     652
     653                  if len(form.final_output_filenames) == 0:
     654                          return True
     655
     656                  items = []
     657                  for fname in form.final_output_filenames:
     658                          item = self.add_file(filename = fname)
     659                          if item is None:
     660                                  for prev_item in items:
     661                                          delete_export_item(pk_export_item = prev_item['pk_export_item'])
     662                                  return False
     663                          items.append(item)
     664                          item['description'] = _('form: %s %s (%s)') % (form.template['name_long'], form.template['external_version'], fname)
     665                          item['designation'] = designation
     666                          item.save()
     667
     668                  return True
     669
     670          #--------------------------------------------------------
     671 -        def add_forms(self, forms=None, designation=None):
     672                  all_ok = True
     673                  for form in forms:
     674                          all_ok = all_ok and self.add_form(form = form, designation = designation)
     675
     676                  return all_ok
     677
     678          #--------------------------------------------------------
     679 -        def add_path(self, path, comment=None):
     680                  """Add a DIR entry to the export area.
     681
     682                  This sort of entry points to a certain directory on a
     683                  certain machine. The content of the the directory
     684                  will be included in exports, the directory *itself*
     685                  will not. For *that*, use a disposable top-level
     686                  directory into which you put the directory to include
     687                  as a subdirectory.
     688                  """
     689                  assert (os.path.isdir(path)), '<path> must exist: %s' % path
     690
     691                  path_item_data = 'DIR::%s::%s/' % (platform.node(), path.rstrip('/'))
     692                  _log.debug('attempting to add path item [%s]', path_item_data)
     693                  item = self.path_item_exists(path_item_data)
     694                  if item is not None:
     695                          _log.debug('[%s] already in export area', path)
     696                          return item
     697
     698                  if comment is None:
     699                          comment = _('path [%s/] on computer "%s"') % (
     700                                  path.rstrip('/'),
     701                                  platform.node()
     702                          )
     703                  else:
     704                          comment += _(' (on "%s")') % platform.node()
     705
     706                  item = create_export_item (
     707                          description = comment,
     708                          pk_identity = self.__pk_identity,
     709                          filename = path_item_data
     710                  )
     711                  try:
     712                          README = open(os.path.join(path, DIRENTRY_README_NAME), mode = 'wt', encoding = 'utf8')
     713                          README.write('GNUmed DIRENTRY information\n')
     714                          README.write('created: %s\n' % gmDateTime.pydt_now_here())
     715                          README.write('machine: %s\n' % platform.node())
     716                          README.write('path: %s\n' % path)
     717                          README.close()
     718                  except OSError:
     719                          _log.exception('READONLY DIRENTRY [%s]', path)
     720
     721                  return item
     722
     723          #--------------------------------------------------------
     724 -        def path_item_exists(self, path_item_data):
     725
     726                  assert (path_item_data.startswith('DIR::')), 'invalid <path_item_data> [%s]' % path_item_data
     727
     728                  where_parts = [
     729                          'pk_identity = %(pat)s',
     730                          'filename = %(fname)s',
     731                          'pk_doc_obj IS NULL'
     732                  ]
     733                  args = {
     734                          'pat': self.__pk_identity,
     735                          'fname': path_item_data
     736                  }
     737                  SQL = _SQL_get_export_items % ' AND '.join(where_parts)
     738                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': SQL, 'args': args}], get_col_idx = True)
     739                  if len(rows) == 0:
     740                          return None
     741
     742                  r = rows[0]
     743                  return cExportItem(row = {'data': r, 'idx': idx, 'pk_field': 'pk_export_item'})
     744
     745          #--------------------------------------------------------
     746 -        def add_file(self, filename=None, hint=None):
     747                  try:
     748                          open(filename).close()
     749                  except Exception:
     750                          _log.exception('cannot open file <%s>', filename)
     751                          return None
     752
     753                  file_md5 = gmTools.file2md5(filename = filename, return_hex = True)
     754                  existing_item = self.md5_exists(md5 = file_md5, include_document_parts = False)
     755                  if existing_item is not None:
     756                          _log.debug('md5 match (%s): %s already in export area', file_md5, filename)
     757                          return existing_item
     758
     759                  path, basename = os.path.split(filename)
     760                  item = create_export_item (
     761                          description = '%s: %s (%s/)' % (
     762                                  gmTools.coalesce(hint, _('file'), '%s'),
     763                                  basename,
     764                                  path
     765                          ),
     766                          pk_identity = self.__pk_identity,
     767                          filename = filename
     768                  )
     769
     770                  if item.update_data_from_file(filename = filename):
     771                          return item
     772
     773                  # failed to insert data, hence remove export item entry
     774                  delete_export_item(pk_export_item = item['pk_export_item'])
     775                  return None
     776
     777          #--------------------------------------------------------
     778 -        def add_files(self, filenames=None, hint=None):
     779                  all_ok = True
     780                  for fname in filenames:
     781                          all_ok = all_ok and (self.add_file(filename = fname, hint = hint) is not None)
     782
     783                  return all_ok
     784
     785          #--------------------------------------------------------
     786 -        def add_documents(self, documents=None):
     787                  for doc in documents:
     788                          doc_tag = _('%s (%s)%s') % (
     789                                  doc['l10n_type'],
     790                                  gmDateTime.pydt_strftime(doc['clin_when'], '%Y %b %d'),
     791                                  gmTools.coalesce(doc['comment'], '', ' "%s"')
     792                          )
     793                          for obj in doc.parts:
     794                                  if self.document_part_item_exists(pk_part = obj['pk_obj']):
     795                                          continue
     796                                  f_ext = ''
     797                                  if obj['filename'] is not None:
     798                                          f_ext = os.path.splitext(obj['filename'])[1].strip('.').strip()
     799                                  if f_ext != '':
     800                                          f_ext = ' .' + f_ext.upper()
     801                                  obj_tag = _('part %s (%s%s)%s') % (
     802                                          obj['seq_idx'],
     803                                          gmTools.size2str(obj['size']),
     804                                          f_ext,
     805                                          gmTools.coalesce(obj['obj_comment'], '', ' "%s"')
     806                                  )
     807                                  create_export_item (
     808                                          description = '%s - %s' % (doc_tag, obj_tag),
     809                                          pk_doc_obj = obj['pk_obj']
     810                                  )
     811
     812          #--------------------------------------------------------
     813 -        def document_part_item_exists(self, pk_part=None):
     814                  cmd = "SELECT EXISTS (SELECT 1 FROM clin.export_item WHERE fk_doc_obj = %(pk_obj)s)"
     815                  args = {'pk_obj': pk_part}
     816                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = False)
     817                  return rows[0][0]
     818
     819          #--------------------------------------------------------
     820 -        def md5_exists(self, md5=None, include_document_parts=False):
     821                  where_parts = [
     822                          'pk_identity = %(pat)s',
     823                          'md5_sum = %(md5)s'
     824                  ]
     825                  args = {
     826                          'pat': self.__pk_identity,
     827                          'md5': md5
     828                  }
     829
     830                  if not include_document_parts:
     831                          where_parts.append('pk_doc_obj IS NULL')
     832
     833                  cmd = _SQL_get_export_items % ' AND '.join(where_parts)
     834                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
     835
     836                  if len(rows) == 0:
     837                          return None
     838
     839                  r = rows[0]
     840                  return cExportItem(row = {'data': r, 'idx': idx, 'pk_field': 'pk_export_item'})
     841
     842          #--------------------------------------------------------
     843 -        def remove_item(self, item):
     844                  if item.is_valid_DIRENTRY:
     845                          tag, node, local_fs_path = item['filename'].split('::', 2)
     846                          gmTools.remove_file(os.path.join(local_fs_path, DIRENTRY_README_NAME))
     847                  elif item.is_DIRENTRY:
     848                          return False
     849
     850                  return delete_export_item(pk_export_item = item['pk_export_item'])
     851
     852          #--------------------------------------------------------
     853 -        def dump_items_to_disk(self, base_dir=None, items=None, passphrase=None):
     854                  if items is None:
     855                          items = self.items
     856                  if len(items) == 0:
     857                          return None
     858                  if base_dir is None:
     859                          from Gnumed.business.gmPerson import cPatient
     860                          pat = cPatient(aPK_obj = self.__pk_identity)
     861                          base_dir = os.path.join(gmTools.mk_sandbox_dir(), pat.subdir_name)
     862                  gmTools.mkdir(base_dir)
     863                  _log.debug('dumping export items to: %s', base_dir)
     864                  for item in items:
     865                          if item.save_to_file(directory = base_dir, passphrase = passphrase) is None:
     866                                  return None
     867                  return base_dir
     868
     869          #--------------------------------------------------------
     870 -        def dump_items_to_disk_as_zip(self, base_dir=None, items=None, passphrase=None):
     871                  _log.debug('target dir: %s', base_dir)
     872                  dump_dir = self.dump_items_to_disk(base_dir = base_dir, items = items)
     873                  if dump_dir is None:
     874                          _log.error('cannot dump export area items')
     875                          return None
     876                  zip_file = gmTools.create_zip_archive_from_dir (
     877                          dump_dir,
     878                          comment = _('GNUmed Patient Media'),
     879                          overwrite = True,
     880                          passphrase = passphrase,
     881                          verbose = _cfg.get(option = 'debug')
     882                  )
     883                  if zip_file is None:
     884                          _log.error('cannot zip export area items dump')
     885                          return None
     886                  return zip_file
     887
     888          #--------------------------------------------------------
     889 -        def export(self, base_dir=None, items=None, passphrase=None):
     890
     891                  if items is None:
     892                          items = self.items
     893                  if len(items) == 0:
     894                          return None
     895
     896                  from Gnumed.business.gmPerson import cPatient
     897                  pat = cPatient(aPK_obj = self.__pk_identity)
     898                  target_base_dir = base_dir
     899                  if target_base_dir is None:
     900                          target_base_dir = gmTools.mk_sandbox_dir(prefix = '%s-' % pat.subdir_name)
     901                  gmTools.mkdir(target_base_dir)
     902                  _log.debug('patient media base dir: %s', target_base_dir)
     903                  if not gmTools.dir_is_empty(target_base_dir):
     904                          _log.error('patient media base dir is not empty')
     905                          return False
     906
     907                  from Gnumed.business.gmPraxis import gmCurrentPraxisBranch
     908                  prax = gmCurrentPraxisBranch()
     909
     910                  html_data = {}
     911
     912                  # 1) assemble everything in a sandbox
     913                  # - setup sandbox
     914                  sandbox_dir = gmTools.mk_sandbox_dir()
     915                  _log.debug('sandbox dir: %s', sandbox_dir)
     916                  doc_dir = os.path.join(sandbox_dir, DOCUMENTS_SUBDIR)
     917                  gmTools.mkdir(doc_dir)
     918                  # - export mugshot
     919                  mugshot = pat.document_folder.latest_mugshot
     920                  if mugshot is not None:
     921                          mugshot_fname = mugshot.save_to_file(directory = doc_dir, adjust_extension = True)
     922                          fname = os.path.split(mugshot_fname)[1]
     923                          html_data['mugshot_url'] = os.path.join(DOCUMENTS_SUBDIR, fname)
     924                          html_data['mugshot_alt'] =_('patient photograph from %s') % gmDateTime.pydt_strftime(mugshot['date_generated'], '%B %Y')
     925                          html_data['mugshot_title'] = gmDateTime.pydt_strftime(mugshot['date_generated'], '%B %Y')
     926                  # - export patient demographics as GDT/XML/VCF/MCF
     927                  pat.export_as_gdt(filename = os.path.join(sandbox_dir, 'patient.gdt'))
     928                  pat.export_as_xml_linuxmednews(filename = os.path.join(sandbox_dir, 'patient.xml'))
     929                  pat.export_as_vcard(filename = os.path.join(sandbox_dir, 'patient.vcf'))
     930                  pat.export_as_mecard(filename = os.path.join(sandbox_dir, u'patient.mcf'))
     931                  # - create CD.INF
     932                  self._create_cd_inf(pat, sandbox_dir)
     933                  # - export items
     934                  docs_list = []
     935                  for item in items:
     936                          # if it is a dicomdir - put it into the root of the target media
     937                          if item.is_DICOM_directory:
     938                                  _log.debug('exporting DICOMDIR DIRENTRY')
     939                                  # save into base dir
     940                                  item_fname = item.save_to_file(directory = sandbox_dir)
     941                                  # do not include into ./documents/ listing
     942                                  continue
     943                          if item.is_valid_DIRENTRY:
     944                                  _log.debug('exporting DIRENTRY')
     945                                  # if there are files in the root dir: put it into a
     946                                  # subdir of ./documents/ where subdir is the leaf
     947                                  # of the the item .filename
     948                                  if item.has_files_in_root:
     949                                          tag, node, local_fs_path = item['filename'].split('::', 2)
     950                                          subdir = local_fs_path.rstrip('/').split('/')[-1]
     951                                          subdir = os.path.join(doc_dir, subdir)
     952                                          gmTools.mkdir(subdir)
     953                                          item_fname = item.save_to_file(directory = subdir)
     954                                  # if it is subdirs only - put it into documents/ directly
     955                                  else:
     956                                          item_fname = item.save_to_file(directory = doc_dir)
     957                                  # include into ./documents/ listing
     958                                  fname = os.path.split(item_fname)[1]
     959                                  docs_list.append([fname, gmTools.html_escape_string(item['description'])])
     960                                  continue
     961                          if item.is_DIRENTRY:
     962                                  # DIRENTRY but not valid: skip
     963                                  continue
     964                          # normal entry, doc obj links or data item
     965                          item_fname = item.save_to_file(directory = doc_dir)
     966                          fname = os.path.split(item_fname)[1]
     967                          # collect items so we can later correlate items and descriptions
     968                          # actually we should link to encrypted items, and to unencrypted ones
     969                          docs_list.append([fname, gmTools.html_escape_string(item['description'])])
     970                  # - link to DICOMDIR if exists
     971                  if 'DICOMDIR' in os.listdir(sandbox_dir):       # in root path
     972                          has_dicomdir = True
     973                          html_data['browse_dicomdir'] = '<li><a href="./DICOMDIR">%s</a></li>' % _('show DICOMDIR file')
     974                  else:
     975                          has_dicomdir = False
     976                  # - include DWV link if there is a DICOMDIR
     977                  if has_dicomdir:
     978                          # do not clone into media base to avoid encryption,
     979                          # DWV will be cloned to there later on
     980                          dwv_sandbox_dir = self._clone_dwv()
     981                          if dwv_sandbox_dir is not None:
     982                                  html_data['run_dicom_viewer'] = '<li><a href="./dwv/viewers/mobile-local/index.html">%s</a></li>' % _('run Radiology Images (DICOM) Viewer')
     983                  # - create frontpage.html
     984                  frontpage_fname = self._create_frontpage_html(pat, prax, sandbox_dir, html_data, docs_list)
     985                  # - start.html (just a copy of frontpage.html)
     986                  #   (later overwritten, if encryption is requested)
     987                  start_fname = os.path.join(sandbox_dir, 'start.html')
     988                  try:
     989                          shutil.copy2(frontpage_fname, start_fname)
     990                  except Exception:
     991                          _log.exception('cannot copy %s to %s', frontpage_fname, start_fname)
     992                  # - start.html (just a convenience copy of frontpage.html)
     993                  start_fname = os.path.join(sandbox_dir, 'start.html')
     994                  try:
     995                          shutil.copy2(frontpage_fname, start_fname)
     996                  except Exception:
     997                          _log.exception('cannot copy %s to %s', frontpage_fname, start_fname)
     998                  # - index.html (just a convenience copy of frontpage.html, overwritten if encryption is requested)
     999                  index_fname = os.path.join(sandbox_dir, 'index.html')
    1000                  try:
    1001                          shutil.copy2(frontpage_fname, index_fname)
    1002                  except Exception:
    1003                          _log.exception('cannot copy %s to %s', frontpage_fname, index_fname)
    1004
    1005                  # 2) encrypt content of sandbox
    1006                  if passphrase is not None:
    1007                          encrypted = gmCrypto.encrypt_directory_content (
    1008                                  directory = sandbox_dir,
    1009                                  receiver_key_ids = None,
    1010                                  passphrase = passphrase,
    1011                                  comment = None,
    1012                                  verbose = _cfg.get(option = 'debug'),
    1013                                  remove_unencrypted = True
    1014                          )
    1015                          if not encrypted:
    1016                                  _log.errror('cannot encrypt data in sandbox dir')
    1017                                  return False
    1018
    1019                  # 3) add never-to-be-encrypted data
    1020                  # - AUTORUN.INF
    1021                  # - README
    1022                  if passphrase is None:
    1023                          self._create_autorun_inf(pat, sandbox_dir)
    1024                          self._create_readme(pat, sandbox_dir)
    1025                  else:
    1026                          self._create_autorun_inf(None, sandbox_dir)
    1027                          self._create_readme(None, sandbox_dir)
    1028                  # - praxis VCF/MCF
    1029                  shutil.move(prax.vcf, os.path.join(sandbox_dir, 'praxis.vcf'))
    1030                  prax.export_as_mecard(filename = os.path.join(sandbox_dir, u'praxis.mcf'))
    1031                  # - include DWV code
    1032                  if has_dicomdir:
    1033                          self._clone_dwv(target_dir = sandbox_dir)
    1034                  # - index.html as boilerplate for decryption
    1035                  if passphrase is not None:
    1036                          index_fname = self._create_index_html(prax, sandbox_dir, html_data)
    1037
    1038                  # 4) move sandbox to target dir
    1039                  target_dir = gmTools.copy_tree_content(sandbox_dir, target_base_dir)
    1040                  if target_dir is None:
    1041                          _log.error('cannot fill target base dir')
    1042                          return False
    1043
    1044                  return target_dir
    1045
    1046          #--------------------------------------------------------
    1047 -        def export_as_zip(self, base_dir=None, items=None, passphrase=None):
    1048                  _log.debug('target dir: %s', base_dir)
    1049                  export_dir = self.export(base_dir = base_dir, items = items)
    1050                  if export_dir is None:
    1051                          _log.debug('cannot export items')
    1052                          return None
    1053                  if passphrase is None:
    1054                          zip_file = gmCrypto.create_zip_archive_from_dir (
    1055                                  export_dir,
    1056                                  comment = _('GNUmed Patient Media'),
    1057                                  overwrite = True,
    1058                                  verbose = _cfg.get(option = 'debug')
    1059                          )
    1060                  else:
    1061                          zip_file = gmCrypto.create_encrypted_zip_archive_from_dir (
    1062                                  export_dir,
    1063                                  comment = _('GNUmed Patient Media'),
    1064                                  overwrite = True,
    1065                                  passphrase = passphrase,
    1066                                  verbose = _cfg.get(option = 'debug')
    1067                          )
    1068                  if zip_file is None:
    1069                          _log.debug('cannot create zip archive')
    1070                          return None
    1071                  return zip_file
    1072
    1073          #--------------------------------------------------------
    1074 -        def _create_index_html(self, praxis, directory, data):
    1075                  # header part
    1076                  _HTML_data = {
    1077                          'html_title_header': _('Patient data'),
    1078                          'title': _('Patient data excerpt'),
    1079                          'docs_title': _('Documents'),
    1080                          'browse_root': _('browse storage medium'),
    1081                          'browse_docs': _('browse documents area'),
    1082                          'doc_subdir': DOCUMENTS_SUBDIR,
    1083                          'date' : gmTools.html_escape_string(gmDateTime.pydt_strftime(gmDateTime.pydt_now_here(), format = '%Y %B %d'))
    1084                  }
    1085                  frontpage_fname_enc = 'frontpage.html.asc'
    1086                  if os.path.isfile(os.path.join(directory, frontpage_fname_enc)):
    1087                          _HTML_data['frontpage_fname'] = frontpage_fname_enc
    1088                  frontpage_fname_enc = 'frontpage.html.7z'
    1089                  if os.path.isfile(os.path.join(directory, frontpage_fname_enc)):
    1090                          _HTML_data['frontpage_fname'] = frontpage_fname_enc
    1091                  # footer part
    1092                  lines = []
    1093                  adr = praxis.branch.org_unit.address
    1094                  if adr is not None:
    1095                          lines.extend(adr.format())
    1096                  for comm in praxis.branch.org_unit.comm_channels:
    1097                          if comm['is_confidential'] is True:
    1098                                  continue
    1099                          lines.append('%s: %s' % (
    1100                                  comm['l10n_comm_type'],
    1101                                  comm['url']
    1102                          ))
    1103                  adr = ''
    1104                  if len(lines) > 0:
    1105                          adr = gmTools.html_escape_string('\n'.join(lines), replace_eol = True, keep_visual_eol = True)
    1106                  _HTML_data['branch'] = gmTools.html_escape_string(praxis['branch'])
    1107                  _HTML_data['praxis'] = gmTools.html_escape_string(praxis['praxis'])
    1108                  _HTML_data['gm_ver'] = gmTools.html_escape_string(gmTools.coalesce(_cfg.get(option = 'client_version'), 'git HEAD'))
    1109                  _HTML_data['adr'] = adr
    1110                  # create file
    1111                  index_fname = os.path.join(directory, 'index.html')
    1112                  index_file = io.open(index_fname, mode = 'wt', encoding = 'utf8')
    1113                  index_file.write(_INDEX_HTML_CONTENT % _HTML_data)
    1114                  index_file.close()
    1115                  return index_fname
    1116
    1117          #--------------------------------------------------------
    1118 -        def _create_frontpage_html(self, patient, praxis, directory, data, docs_list):
    1119                  # <li><a href="documents/filename-1.ext">document 1 description</a></li>
    1120                  _HTML_LIST_ITEM = '     <li><a href="%s">%s</a></li>'
    1121                  # header part
    1122                  _HTML_data = {
    1123                          'html_title_header': _('Patient data for'),
    1124                          'html_title_patient': gmTools.html_escape_string(patient.get_description_gender(with_nickname = False) + ', ' + _('born') + ' ' + patient.get_formatted_dob('%Y %B %d')),
    1125                          'title': _('Patient data excerpt'),
    1126                          'pat_name': gmTools.html_escape_string(patient.get_description_gender(with_nickname = False)),
    1127                          'pat_dob': gmTools.html_escape_string(_('born') + ' ' + patient.get_formatted_dob('%Y %B %d')),
    1128                          'mugshot_url': 'documents/no-such-file.png',
    1129                          'mugshot_alt': _('no patient photograph available'),
    1130                          'mugshot_title': '',
    1131                          'docs_title': _('Documents'),
    1132                          'browse_root': _('browse storage medium'),
    1133                          'browse_docs': _('browse documents area'),
    1134                          'doc_subdir': DOCUMENTS_SUBDIR,
    1135                          'browse_dicomdir': '',
    1136                          'run_dicom_viewer': '',
    1137                          'date' : gmTools.html_escape_string(gmDateTime.pydt_strftime(gmDateTime.pydt_now_here(), format = '%Y %B %d'))
    1138                  }
    1139                  for key in data:
    1140                          _HTML_data[key] = data[key]
    1141
    1142                  # documents part
    1143                  _HTML_docs_list = []
    1144                  for doc in docs_list:
    1145                          subdir = os.path.join(directory, DOCUMENTS_SUBDIR, doc[0])
    1146                          if os.path.isdir(subdir):
    1147                                  _HTML_docs_list.append(_HTML_LIST_ITEM % (os.path.join(DOCUMENTS_SUBDIR, doc[0]), _('DIRECTORY: %s/%s/') % (DOCUMENTS_SUBDIR, doc[0])))
    1148                                  _HTML_docs_list.append('        <ul>')
    1149                                  for fname in os.listdir(subdir):
    1150                                          tmp = os.path.join(subdir, fname)
    1151                                          if os.path.isdir(tmp):
    1152                                                  _HTML_docs_list.append('                <li><a href="%s">%s</a></li>' % (os.path.join(DOCUMENTS_SUBDIR, doc[0], fname), _('DIRECTORY: %s/%s/%s/') % (DOCUMENTS_SUBDIR, doc[0], fname)))
    1153                                          else:
    1154                                                  _HTML_docs_list.append('                <li><a href="%s">%s</a></li>' % (os.path.join(DOCUMENTS_SUBDIR, doc[0], fname), fname))
    1155                                  _HTML_docs_list.append('        </ul>')
    1156                          else:
    1157                                  _HTML_docs_list.append(_HTML_LIST_ITEM % (doc[0], doc[1]))
    1158                  _HTML_data['docs_list'] = u'\n  '.join(_HTML_docs_list)
    1159
    1160                  # footer part
    1161                  lines = []
    1162                  adr = praxis.branch.org_unit.address
    1163                  if adr is not None:
    1164                          lines.extend(adr.format())
    1165                  for comm in praxis.branch.org_unit.comm_channels:
    1166                          if comm['is_confidential'] is True:
    1167                                  continue
    1168                          lines.append('%s: %s' % (
    1169                                  comm['l10n_comm_type'],
    1170                                  comm['url']
    1171                          ))
    1172                  adr = ''
    1173                  if len(lines) > 0:
    1174                          adr = gmTools.html_escape_string('\n'.join(lines), replace_eol = True, keep_visual_eol = True)
    1175                  _HTML_data['branch'] = gmTools.html_escape_string(praxis['branch'])
    1176                  _HTML_data['praxis'] = gmTools.html_escape_string(praxis['praxis'])
    1177                  _HTML_data['gm_ver'] = gmTools.html_escape_string(gmTools.coalesce(_cfg.get(option = 'client_version'), 'git HEAD'))
    1178                  _HTML_data['adr'] = adr
    1179                  # create file
    1180                  frontpage_fname = os.path.join(directory, 'frontpage.html')
    1181                  frontpage_file = io.open(frontpage_fname, mode = 'wt', encoding = 'utf8')
    1182                  frontpage_file.write(_FRONTPAGE_HTML_CONTENT % _HTML_data)
    1183                  frontpage_file.close()
    1184                  return frontpage_fname
    1185
    1186          #--------------------------------------------------------
    1187 -        def _clone_dwv(self, target_dir=None):
    1188                  _log.debug('cloning dwv')
    1189                  # find DWV
    1190                  dwv_src_dir = os.path.join(gmTools.gmPaths().local_base_dir, 'resources', 'dwv4export')
    1191                  if not os.path.isdir(dwv_src_dir):
    1192                          _log.debug('[%s] not found', dwv_src_dir)
    1193                          dwv_src_dir = os.path.join(gmTools.gmPaths().system_app_data_dir, 'resources', 'dwv4export')
    1194                  if not os.path.isdir(dwv_src_dir):
    1195                          _log.debug('[%s] not found', dwv_src_dir)
    1196                          return None
    1197                  # clone it
    1198                  if target_dir is None:
    1199                          target_dir = gmTools.mk_sandbox_dir()
    1200                  dwv_target_dir = os.path.join(target_dir, 'dwv')
    1201                  gmTools.rmdir(dwv_target_dir)
    1202                  try:
    1203                          shutil.copytree(dwv_src_dir, dwv_target_dir)
    1204                  except (shutil.Error, OSError):
    1205                          _log.exception('cannot include DWV, skipping')
    1206                          return None
    1207
    1208                  return dwv_target_dir
    1209
    1210          #--------------------------------------------------------
    1211 -        def _create_readme(self, patient, directory):
    1212                  _README_CONTENT = (
    1213                          'This is a patient data excerpt created by the GNUmed Electronic Medical Record.\n'
    1214                          '\n'
    1215                          'Patient: %s\n'
    1216                          '\n'
    1217                          'Please display <frontpage.html> to browse patient data.\n'
    1218                          '\n'
    1219                          'Individual documents are stored in the subdirectory\n'
    1220                          '\n'
    1221                          '       documents/\n'
    1222                          '\n'
    1223                          '\n'
    1224                          'Data may need to be decrypted with either GNU Privacy\n'
    1225                          'Guard or 7zip/WinZip.\n'
    1226                          '\n'
    1227                          '.asc:\n'
    1228                          '       https://gnupg.org\n'
    1229                          '\n'
    1230                          '.7z:\n'
    1231                          '       https://www.7-zip.org\n'
    1232                          '       https://www.winzip.com\n'
    1233                          '\n'
    1234                          'To obtain any needed keys you will have to get in touch with\n'
    1235                          'the creator or the owner of this patient media excerpt.\n'
    1236                  )
    1237                  readme_fname = os.path.join(directory, 'README')
    1238                  readme_file = io.open(readme_fname, mode = 'wt', encoding = 'utf8')
    1239                  if patient is None:
    1240                          pat_str = _('<protected>')
    1241                  else:
    1242                          pat_str = patient.get_description_gender(with_nickname = False) + ', ' + _('born') + ' ' + patient.get_formatted_dob('%Y %B %d')
    1243                  readme_file.write(_README_CONTENT % pat_str)
    1244                  readme_file.close()
    1245                  return readme_fname
    1246
    1247          #--------------------------------------------------------
    1248 -        def _create_cd_inf(self, patient, directory):
    1249                  _CD_INF_CONTENT = (
    1250                          '[Patient Info]\r\n'                                    # needs \r\n for Windows
    1251                          'PatientName=%s, %s\r\n'
    1252                          'Gender=%s\r\n'
    1253                          'BirthDate=%s\r\n'
    1254                          'CreationDate=%s\r\n'
    1255                          'PID=%s\r\n'
    1256                          'EMR=GNUmed\r\n'
    1257                          'Version=%s\r\n'
    1258                          '#StudyDate=\r\n'
    1259                          '#VNRInfo=<body part>\r\n'
    1260                          '\r\n'
    1261                          '# name format: lastnames, firstnames\r\n'
    1262                          '# date format: YYYY-MM-DD (ISO 8601)\r\n'
    1263                          '# gender format: %s\r\n'
    1264                  )
    1265                  fname = os.path.join(directory, 'CD.INF')
    1266                  cd_inf = io.open(fname, mode = 'wt', encoding = 'utf8')
    1267                  cd_inf.write(_CD_INF_CONTENT % (
    1268                          patient['lastnames'],
    1269                          patient['firstnames'],
    1270                          gmTools.coalesce(patient['gender'], '?'),
    1271                          patient.get_formatted_dob('%Y-%m-%d'),
    1272                          gmDateTime.pydt_strftime(gmDateTime.pydt_now_here(), format = '%Y-%m-%d'),
    1273                          patient.ID,
    1274                          _cfg.get(option = 'client_version'),
    1275                          ' / '.join([ '%s = %s (%s)' % (g['tag'], g['label'], g['l10n_label']) for g in patient.gender_list ])
    1276                  ))
    1277                  cd_inf.close()
    1278                  return fname
    1279
    1280          #--------------------------------------------------------
    1281 -        def _create_autorun_inf(self, patient, directory):
    1282                  _AUTORUN_INF_CONTENT = (                                                        # needs \r\n for Windows
    1283                          '[AutoRun.Amd64]\r\n'                                   # 64 bit
    1284                          'label=%(label)s\r\n'                                   # patient name/DOB
    1285                          'shellexecute=index.html\r\n'
    1286                          'action=%(action)s\r\n'                                 # % _('Browse patient data')
    1287                          '%(icon)s\r\n'                                                  # "icon=gnumed.ico" or ""
    1288                          'UseAutoPlay=1\r\n'
    1289                          '\r\n'
    1290                          '[AutoRun]\r\n'                                                 # 32 bit
    1291                          'label=%(label)s\r\n'                                   # patient name/DOB
    1292                          'shellexecute=index.html\r\n'
    1293                          'action=%(action)s\r\n'                                 # % _('Browse patient data')
    1294                          '%(icon)s\r\n'                                                  # "icon=gnumed.ico" or ""
    1295                          'UseAutoPlay=1\r\n'
    1296                          '\r\n'
    1297                          '[Content]\r\n'
    1298                          'PictureFiles=yes\r\n'
    1299                          'VideoFiles=yes\r\n'
    1300                          'MusicFiles=no\r\n'
    1301                          '\r\n'
    1302                          '[IgnoreContentPaths]\r\n'
    1303                          '\documents\r\n'
    1304                          '\r\n'
    1305                          '[unused]\r\n'
    1306                          'open=requires explicit executable\r\n'
    1307                  )
    1308                  autorun_dict = {
    1309                          'label': self._compute_autorun_inf_label(patient),
    1310                          'action': _('Browse patient data'),
    1311                          'icon': ''
    1312                  }
    1313                  media_icon_kwd = '$$gnumed_patient_media_export_icon'
    1314                  media_icon_kwd_exp = gmKeywordExpansion.get_expansion (
    1315                          keyword = media_icon_kwd,
    1316                          textual_only = False,
    1317                          binary_only = True
    1318                  )
    1319                  icon_tmp_fname = media_icon_kwd_exp.save_to_file (
    1320                          target_mime = 'image/x-icon',
    1321                          target_extension = '.ico',
    1322                          ignore_conversion_problems = True
    1323                  )
    1324                  if icon_tmp_fname is None:
    1325                          _log.error('cannot retrieve <%s>', media_icon_kwd)
    1326                  else:
    1327                          media_icon_fname = os.path.join(directory, 'gnumed.ico')
    1328                          try:
    1329                                  shutil.copy2(icon_tmp_fname, media_icon_fname)
    1330                                  autorun_dict['icon'] = 'icon=gnumed.ico'
    1331                          except Exception:
    1332                                  _log.exception('cannot move %s to %s', icon_tmp_fname, media_icon_fname)
    1333                  autorun_fname = os.path.join(directory, 'AUTORUN.INF')
    1334                  autorun_file = io.open(autorun_fname, mode = 'wt', encoding = 'cp1252', errors = 'replace')
    1335                  autorun_file.write(_AUTORUN_INF_CONTENT % autorun_dict)
    1336                  autorun_file.close()
    1337                  return autorun_fname
    1338
    1339          #--------------------------------------------------------
    1340 -        def _compute_autorun_inf_label(self, patient):
    1341                  if patient is None:
    1342                          # if no patient provided - assume the target media
    1343                          # is to be encrypted and thusly do not expose patient data,
    1344                          # AUTORUN.INF itself will not be encrypted because
    1345                          # Windows must be able to parse it
    1346                          return _('GNUmed patient data excerpt')[:32]
    1347
    1348                  LABEL_MAX_LEN = 32
    1349                  dob = patient.get_formatted_dob(format = ' %Y%m%d', none_string = '', honor_estimation = False)
    1350                  if dob == '':
    1351                          gender_template = ' (%s)'
    1352                  else:
    1353                          gender_template = ' %s'
    1354                  gender = gmTools.coalesce(patient['gender'], '', gender_template)
    1355                  name_max_len = LABEL_MAX_LEN - len(gender) - len(dob)                   # they already include appropriate padding
    1356                  name = patient.active_name
    1357                  last = name['lastnames'].strip()
    1358                  first = name['firstnames'].strip()
    1359                  len_last = len(last)
    1360                  len_first = len(first)
    1361                  while (len_last + len_first + 1) > name_max_len:
    1362                          if len_first > 6:
    1363                                  len_first -= 1
    1364                                  if first[len_first - 1] == ' ':
    1365                                          len_first -= 1
    1366                                  continue
    1367                          len_last -= 1
    1368                          if last[len_last - 1] == ' ':
    1369                                  len_last -= 1
    1370                  last = last[:len_last].strip().upper()
    1371                  first = first[:len_first].strip()
    1372                  # max 32 chars, supposedly ASCII, but CP1252 likely works pretty well
    1373                  label = (('%s %s%s%s' % (last, first, dob,      gender)).strip())[:32]
    1374                  return label
    1375
    1376          #--------------------------------------------------------
    1377 -        def __encrypt_file(self, filename, passphrase=None):
    1378                  if passphrase is None:
    1379                          return filename
    1380                  enc_filename = gmCrypto.encrypt_file (
    1381                          filename = filename,
    1382                          passphrase = passphrase,
    1383                          verbose = _cfg.get(option = 'debug')
    1384                  )
    1385                  if enc_filename is None:
    1386                          _log.error('cannot encrypt')
    1387                          return None
    1388                  if not gmTools.remove_file(filename, log_error = True, force = True):
    1389                          _log.error('cannot remove unencrypted file')
    1390                          return None
    1391                  return enc_filename
    1392
    1393          #--------------------------------------------------------
    1394          # properties
    1395          #--------------------------------------------------------
    1396 -        def get_items(self, designation=None, order_by='designation, description'):
    1397                  return get_export_items(order_by = order_by, pk_identity = self.__pk_identity, designation = designation)
    1398
    1399          items = property(get_items, lambda x:x)
    1400
    1401          #--------------------------------------------------------
    1402 -        def get_printouts(self, order_by='designation, description'):
    1403                  return get_print_jobs(order_by = order_by, pk_identity = self.__pk_identity)
    1404
    1405          printouts = property(get_printouts, lambda x:x)
    1406
    1407  #============================================================
    1408  if __name__ == '__main__':
    1409
    1410          if len(sys.argv) < 2:
    1411                  sys.exit()
    1412
    1413          if sys.argv[1] != 'test':
    1414                  sys.exit()
    1415
    1416          from Gnumed.pycommon import gmI18N
    1417          gmI18N.activate_locale()
    1418          gmI18N.install_domain()
    1419
    1420          from Gnumed.business import gmPraxis
    1421
    1422          #---------------------------------------
    1423 -        def test_export_items():
    1424  #               items = get_export_items()
    1425  #               for item in items:
    1426  #                       print item.format()
    1427                  import random
    1428                  create_export_item(description = 'description %s' % random.random(), pk_identity = 12, pk_doc_obj = None, filename = 'dummy.dat')
    1429                  items = get_export_items()
    1430                  for item in items:
    1431                          print(item.format())
    1432                  item['pk_doc_obj'] = 1
    1433                  item.save()
    1434                  print(item)
    1435
    1436          #---------------------------------------
    1437 -        def test_export_area():
    1438                  exp = cExportArea(12)
    1439                  #print exp.export_with_meta_data()
    1440                  #print exp.items
    1441                  #exp.add_file(sys.argv[2])
    1442                  prax = gmPraxis.gmCurrentPraxisBranch(branch = gmPraxis.cPraxisBranch(1))
    1443                  #print(prax)
    1444                  #print(prax.branch)
    1445                  try:
    1446                          pwd = sys.argv[2]
    1447                  except IndexError:
    1448                          pwd = None
    1449                  print(exp.export(passphrase = pwd))
    1450
    1451          #---------------------------------------
    1452 -        def test_label():
    1453
    1454                  from Gnumed.business.gmPerson import cPatient
    1455                  from Gnumed.business.gmPersonSearch import ask_for_patient
    1456
    1457                  #while ask_for_patient() is not None:
    1458                  pat_min = 1
    1459                  pat_max = 100
    1460                  try:
    1461                          pat_min = int(sys.argv[2])
    1462                          pat_max = int(sys.argv[3])
    1463                  except:
    1464                          pass
    1465                  cPatient(aPK_obj = pat_min)
    1466                  f = io.open('x-auto_inf_labels.txt', mode = 'w', encoding = 'utf8')
    1467                  f.write('--------------------------------\n')
    1468                  f.write('12345678901234567890123456789012\n')
    1469                  f.write('--------------------------------\n')
    1470                  for pat_id in range(pat_min, pat_max):
    1471                          try:
    1472                                  exp_area = cExportArea(pat_id)
    1473                                  pat = cPatient(aPK_obj = pat_id)
    1474                          except:
    1475                                  continue
    1476                          f.write(exp_area._compute_autorun_inf_label(pat) + '\n')
    1477                  f.close()
    1478                  return
    1479
    1480          #---------------------------------------
    1481          #test_export_items()
    1482          test_export_area()
    1483          #test_label()
    1484
    1485          sys.exit(0)
    1486
    1487  #============================================================
    1488  # CDROM "run.bat":
    1489  #
    1490  #@echo off
    1491  #
    1492  #if defined ProgramFiles(x86) (
    1493  #    ::64-bit
    1494  #    start /B x64\mdicom.exe /scan .
    1495  #) else (
    1496  #    ::32-bit
    1497  #    start /B win32\mdicom.exe /scan .
    1498  #)
    1499  #
    1500  #--------------------------------------------------
    1501