Gnumed.business.gmExportArea

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