Gnumed.wxpython.gmMacro

Package Gnumed :: Package wxpython :: Module gmMacro |
[frames] | no frames]

# Source Code for Module Gnumed.wxpython.gmMacro

       1  # -*- coding: utf-8 -*-
       2  """GNUmed macro primitives.
       3
       4  This module implements functions a macro can legally use.
       5  """
       6  #=====================================================================
       7  __author__ = "K.Hilbert <karsten.hilbert@gmx.net>"
       8
       9  import sys
      10  import time
      11  import random
      12  import types
      13  import logging
      14  import os
      15  import io
      16  import datetime
      17  import urllib.parse
      18  import codecs
      19  import re as regex
      20
      21
      22  import wx
      23
      24
      25  if __name__ == '__main__':
      26          sys.path.insert(0, '../../')
      27  from Gnumed.pycommon import gmI18N
      28  if __name__ == '__main__':
      29          gmI18N.activate_locale()
      30          gmI18N.install_domain()
      31  from Gnumed.pycommon import gmGuiBroker
      32  from Gnumed.pycommon import gmTools
      33  from Gnumed.pycommon import gmBorg
      34  from Gnumed.pycommon import gmExceptions
      35  from Gnumed.pycommon import gmCfg2
      36  from Gnumed.pycommon import gmDateTime
      37  from Gnumed.pycommon import gmMimeLib
      38  from Gnumed.pycommon import gmShellAPI
      39  from Gnumed.pycommon import gmCrypto
      40
      41  from Gnumed.business import gmPerson
      42  from Gnumed.business import gmStaff
      43  from Gnumed.business import gmDemographicRecord
      44  from Gnumed.business import gmMedication
      45  from Gnumed.business import gmPathLab
      46  from Gnumed.business import gmPersonSearch
      47  from Gnumed.business import gmVaccination
      48  from Gnumed.business import gmKeywordExpansion
      49  from Gnumed.business import gmPraxis
      50
      51  from Gnumed.wxpython import gmGuiHelpers
      52  from Gnumed.wxpython import gmNarrativeWorkflows
      53  from Gnumed.wxpython import gmPatSearchWidgets
      54  from Gnumed.wxpython import gmPersonContactWidgets
      55  from Gnumed.wxpython import gmPlugin
      56  from Gnumed.wxpython import gmEMRStructWidgets
      57  from Gnumed.wxpython import gmEncounterWidgets
      58  from Gnumed.wxpython import gmListWidgets
      59  from Gnumed.wxpython import gmDemographicsWidgets
      60  from Gnumed.wxpython import gmDocumentWidgets
      61  from Gnumed.wxpython import gmKeywordExpansionWidgets
      62  from Gnumed.wxpython import gmPraxisWidgets
      63  from Gnumed.wxpython import gmAddressWidgets
      64
      65
      66  _log = logging.getLogger('gm.scripting')
      67  _cfg = gmCfg2.gmCfgData()
      68
      69  #=====================================================================
      70  # values for the following placeholders must be injected from the outside before
      71  # using them, in use they must conform to the "placeholder::::max length" syntax,
      72  # as long as they resolve to None they return their respective names so the
      73  # developers can know which placeholder was not set
      74  known_injectable_placeholders = [
      75          'form_name_long',
      76          'form_name_short',
      77          'form_version',
      78          'form_version_internal',
      79          'form_last_modified'
      80  ]
      81
      82  # the following must satisfy the pattern "$<name::args::(optional) max string length>$" when used
      83  __known_variant_placeholders = {
      84          # generic:
      85          'free_text': u"""show a dialog for entering some free text:
      86                  args: <message>//<preset>
      87                          <message>: shown in input dialog, must not contain either
      88                                  of '::' and whatever the arguments divider is
      89                                  set to (default '//'),
      90                          <preset>: whatever to initially show inside the input field,
      91                  caches input per <message>""",
      92
      93          'text_snippet': """a text snippet, taken from the keyword expansion mechanism:
      94                  args: <snippet name>//<template>""",
      95
      96          'data_snippet': """a binary snippet, taken from the keyword expansion mechanism:
      97                  args: <snippet name>//<template>//<optional target mime type>//<optional target extension>
      98                  returns full path to an exported copy of the
      99                  data rather than the data itself,
     100                  template: string template for outputting the path
     101                  target mime type: a mime type into which to convert the image, no conversion if not given
     102                  target extension: target file name extension, derived from target mime type if not given
     103          """,
     104          u'qrcode': u"""generate QR code file for a text snippet:
     105                  returns: path to QR code png file
     106                  args: <text>//<template>
     107                          text: utf8 text, will always be encoded in 'binary' mode with utf8 as encoding
     108                          template: %s-template into which to insert the QR code png file path
     109          """,
     110
     111          # text manipulation
     112          'range_of': """select range of enclosed text (note that this cannot take into account non-length characters such as enclosed LaTeX code
     113                  args: <enclosed text>
     114          """,
     115          'if_not_empty': """format text based on template if not empty
     116                  args: <possibly-empty-text>//<template-if-not-empty>//<alternative-text-if-empty>
     117          """,
     118
     119          u'tex_escape': u"args: string to escape, mostly obsolete now",
     120
     121          u'url_escape': u"""Escapes a string suitable for use as _data_ in an URL
     122                  args: text to escape
     123          """,
     124
     125          # internal state
     126          'ph_cfg': u"""Set placeholder handler options.
     127                  args: option name//option value//macro return string
     128                  option names:
     129                          ellipsis: what to use as ellipsis (if anything) when
     130                                  shortening strings or string regions, setting the
     131                                  value to NONE will switch off ellipis handling,
     132                                  default is switched off
     133                          argumentsdivider: what to use as divider when splitting
     134                                  an argument string into parts, default is '//',
     135                                  note that the 'config' placeholder will ALWAYS
     136                                  use '//' to split its argument string, regardless
     137                                  of which setting of <argumentsdivider> is in effect,
     138                                  use DEFAULT to reset this setting back to the
     139                                  default '//'
     140                          encoding: the encoding in which data emitted by GNUmed
     141                                  as placeholder replacement needs to be valid in,
     142                                  note that GNUmed will still emit unicode to replacement
     143                                  consumers but it will ensure the data emitted _can_
     144                                  be encoded by this target encoding (by roundtripping
     145                                  unicode-encoding-unicode)
     146                                  valid from where this placeholder is located at
     147                                  until further change,
     148                                  use DEFAULT to reset encoding back to the default
     149                                  which is to not ensure compatibility,
     150                                  if the encoding ends in '-strict' then the placeholder
     151                                  replacement will fail if the roundtrip fails
     152          """,
     153          u'if_debugging': u"""set text based on whether debugging is active
     154                  args: <text-if-debugging>//<text-if-not-debugging>
     155          """,
     156
     157          'today': "args: strftime format",
     158
     159          'gender_mapper': """maps gender of patient to a string:
     160                  args: <value when person is male> // <is female> // <is other>
     161                  eg. 'male//female//other'
     162                  or: 'Lieber Patient//Liebe Patientin'""",
     163          'client_version': "the version of the current client as a string (no 'v' in front)",
     164
     165          'gen_adr_street': """part of a generic address, cached, selected from database:
     166                  args: optional template//optional selection message//optional cache ID
     167                  template: %s-style formatting template
     168                  message: text message shown in address selection list
     169                  cache ID: used to differentiate separate cached invocations of this placeholder
     170          """,
     171          'gen_adr_number': """part of a generic address, cached, selected from database:
     172                  args: optional template//optional selection message//optional cache ID
     173                  template: %s-style formatting template
     174                  message: text message shown in address selection list
     175                  cache ID: used to differentiate separate cached invocations of this placeholder
     176          """,
     177          'gen_adr_subunit': """part of a generic address, cached, selected from database:
     178                  args: optional template//optional selection message//optional cache ID
     179                  template: %s-style formatting template
     180                  message: text message shown in address selection list
     181                  cache ID: used to differentiate separate cached invocations of this placeholder
     182          """,
     183          'gen_adr_location': """part of a generic address, cached, selected from database:
     184                  args: optional template//optional selection message//optional cache ID
     185                  template: %s-style formatting template
     186                  message: text message shown in address selection list
     187                  cache ID: used to differentiate separate cached invocations of this placeholder
     188          """,
     189          'gen_adr_suburb': """part of a generic address, cached, selected from database:
     190                  args: optional template//optional selection message//optional cache ID
     191                  template: %s-style formatting template
     192                  message: text message shown in address selection list
     193                  cache ID: used to differentiate separate cached invocations of this placeholder
     194          """,
     195          'gen_adr_postcode': """part of a generic address, cached, selected from database:
     196                  args: optional template//optional selection message//optional cache ID
     197                  template: %s-style formatting template
     198                  message: text message shown in address selection list
     199                  cache ID: used to differentiate separate cached invocations of this placeholder
     200          """,
     201          'gen_adr_region': """part of a generic address, cached, selected from database:
     202                  args: optional template//optional selection message//optional cache ID
     203                  template: %s-style formatting template
     204                  message: text message shown in address selection list
     205                  cache ID: used to differentiate separate cached invocations of this placeholder
     206          """,
     207          'gen_adr_country': """part of a generic address, cached, selected from database:
     208                  args: optional template//optional selection message//optional cache ID
     209                  template: %s-style formatting template
     210                  message: text message shown in address selection list
     211                  cache ID: used to differentiate separate cached invocations of this placeholder
     212          """,
     213
     214          'receiver_name': """the receiver name, cached, selected from database:
     215                  receivers are presented for selection from people/addresses related
     216                  to the patient in some way or other,
     217                  args: optional template//optional cache ID
     218                  template: %s-style formatting template
     219                  cache ID: used to differentiate separate cached invocations of this placeholder
     220          """,
     221          'receiver_street': """part of a receiver address, cached, selected from database:
     222                  receivers are presented for selection from people/addresses related
     223                  to the patient in some way or other,
     224                  args: optional template//optional cache ID
     225                  template: %s-style formatting template
     226                  cache ID: used to differentiate separate cached invocations of this placeholder
     227          """,
     228          'receiver_number': """part of a receiver address, cached, selected from database:
     229                  receivers are presented for selection from people/addresses related
     230                  to the patient in some way or other,
     231                  args: optional template//optional cache ID
     232                  template: %s-style formatting template
     233                  cache ID: used to differentiate separate cached invocations of this placeholder
     234          """,
     235          'receiver_subunit': """part of a receiver address, cached, selected from database:
     236                  receivers are presented for selection from people/addresses related
     237                  to the patient in some way or other,
     238                  args: optional template//optional cache ID
     239                  template: %s-style formatting template
     240                  cache ID: used to differentiate separate cached invocations of this placeholder
     241          """,
     242          'receiver_location': """part of a receiver address, cached, selected from database:
     243                  receivers are presented for selection from people/addresses related
     244                  to the patient in some way or other,
     245                  args: optional template//optional cache ID
     246                  template: %s-style formatting template
     247                  cache ID: used to differentiate separate cached invocations of this placeholder
     248          """,
     249          'receiver_suburb': """part of a receiver address, cached, selected from database:
     250                  receivers are presented for selection from people/addresses related
     251                  to the patient in some way or other,
     252                  args: optional template//optional cache ID
     253                  template: %s-style formatting template
     254                  cache ID: used to differentiate separate cached invocations of this placeholder
     255          """,
     256          'receiver_postcode': """part of a receiver address, cached, selected from database:
     257                  receivers are presented for selection from people/addresses related
     258                  to the patient in some way or other,
     259                  args: optional template//optional cache ID
     260                  template: %s-style formatting template
     261                  cache ID: used to differentiate separate cached invocations of this placeholder
     262          """,
     263          'receiver_region': """part of a receiver address, cached, selected from database:
     264                  receivers are presented for selection from people/addresses related
     265                  to the patient in some way or other,
     266                  args: optional template//optional cache ID
     267                  template: %s-style formatting template
     268                  cache ID: used to differentiate separate cached invocations of this placeholder
     269          """,
     270          'receiver_country': """part of a receiver address, cached, selected from database:
     271                  receivers are presented for selection from people/addresses related
     272                  to the patient in some way or other,
     273                  args: optional template//optional cache ID
     274                  template: %s-style formatting template
     275                  cache ID: used to differentiate separate cached invocations of this placeholder
     276          """,
     277
     278          # patient demographics:
     279          'name': "args: template for name parts arrangement",
     280          'date_of_birth': "args: strftime date/time format directive",
     281
     282          'patient_address': "args: <type of address>//<optional formatting template>",
     283          'adr_street': "args: <type of address>, cached per type",
     284          'adr_number': "args: <type of address>, cached per type",
     285          'adr_subunit': "args: <type of address>, cached per type",
     286          'adr_location': "args: <type of address>, cached per type",
     287          'adr_suburb': "args: <type of address>, cached per type",
     288          'adr_postcode': "args: <type of address>, cached per type",
     289          'adr_region': "args: <type of address>, cached per type",
     290          'adr_country': "args: <type of address>, cached per type",
     291
     292          'patient_comm': "args: <comm channel type as per database>//<%(field)s-template>",
     293
     294          'patient_vcf': """returns path to VCF for current patient
     295                  args: <template>
     296                  template: %s-template for path
     297          """,
     298          'patient_gdt': """returns path to GDT for current patient
     299                  args: <template>
     300                  template: %s-template for path
     301          """,
     302          u'patient_mcf': u"""returns MECARD for current patient
     303                  args: <format>//<template>
     304                  format: fmt=qr|mcf|txt
     305                          qr: QR code png file path,
     306                          mcf: MECARD .mcf file path,
     307                          txt: MECARD string,
     308                          default - if omitted - is "txt",
     309                  template: tmpl=<%s-template string>, "%s" if omitted
     310          """,
     311
     312          'patient_tags': "args: <%(field)s-template>//<separator>",
     313          #u'patient_tags_table': u"no args",
     314          'patient_photo': """outputs URL to exported patient photo (cached per mime type and extension):
     315                  args: <template>//<optional target mime type>//<optional target extension>,
     316                  returns full path to an exported copy of the
     317                  image rather than the image data itself,
     318                  returns u'' if no mugshot available,
     319                  template: string template for outputting the path
     320                  target mime type: a mime type into which to convert the image, no conversion if not given
     321                  target extension: target file name extension, derived from target mime type if not given""",
     322          'external_id': "args: <type of ID>//<issuer of ID>",
     323
     324
     325          # clinical record related:
     326          'soap': "get all of SOAPU/ADMIN, no template in args needed",
     327          'soap_s': "get subset of SOAPU/ADMIN, no template in args needed",
     328          'soap_o': "get subset of SOAPU/ADMIN, no template in args needed",
     329          'soap_a': "get subset of SOAPU/ADMIN, no template in args needed",
     330          'soap_p': "get subset of SOAPU/ADMIN, no template in args needed",
     331          'soap_u': "get subset of SOAPU/ADMIN, no template in args needed",
     332          'soap_admin': "get subset of SOAPU/ADMIN, no template in args needed",
     333
     334          'progress_notes': """get progress notes:
     335                  args: categories//template
     336                  categories: string with 'soapu '; ' ' == None == admin
     337                  template:       u'something %s something'               (do not include // in template !)""",
     338
     339          'soap_for_encounters': """lets the user select a list of encounters for which:
     340                  LaTeX formatted progress notes are emitted,
     341                  args: soap categories // strftime date format""",
     342
     343          'soap_by_issue': """lets the user select a list of issues and then SOAP entries from those issues:
     344                  args: soap categories // strftime date format // template""",
     345
     346          'soap_by_episode': """lets the user select a list of episodes and then SOAP entries from those episodes:
     347                  args: soap categories // strftime date format // template""",
     348
     349          'emr_journal': """returns EMR journal view entries:
     350                  args format:   <categories>//<template>//<line length>//<time range>//<target format>
     351                  categories:        string with any of "s", "o", "a", "p", "u", " "; (" " == None == admin category)
     352                  template:          something %s something else (Do not include // in the template !)
     353                  line length:   the maximum length of individual lines, not the total placeholder length
     354                  time range:             the number of weeks going back in time if given as a single number, or else it must be a valid PostgreSQL interval definition (w/o the ::interval)""",
     355
     356          'substance_abuse': """returns substance abuse entries:
     357                  args: line template
     358          """,
     359
     360          'current_meds': """returns current medications:
     361                  args: line template//<select>
     362                  <select>: if this is present the user will be asked which meds to export""",
     363
     364          'current_meds_for_rx': """formats substance intakes either by substance (non-product intakes) or by producdt (once per product intake, even if multi-component):
     365                  args: <line template>
     366                  <line_template>: template into which to insert each intake, keys from
     367                  clin.v_substance_intakes, special additional keys:
     368                          %(contains)s -- list of components
     369                          %(amount2dispense)s -- how much/many to dispense""",
     370
     371          'current_meds_AMTS': """emit LaTeX longtable lines with appropriate page breaks:
     372                  also creates per-page AMTS QR codes and sets the
     373                  following internal placeholders:
     374                          amts_png_file_1
     375                          amts_png_file_2
     376                          amts_png_file_3
     377                          amts_data_file_1
     378                          amts_data_file_2
     379                          amts_data_file_3
     380                          amts_png_file_current_page
     381                          amts_data_file_utf8
     382                          amts_png_file_utf8
     383                  the last of which contains the LaTeX command \\thepage (such that
     384                  LaTeX can use this in, say, page headers) but omitting the .png
     385                  (for which LaTeX will look by itself),
     386                  note that you will have to use the 2nd- or 3rd-pass placeholder
     387                  format if you plan to insert the above because they will only be
     388                  available by first (or second) pass processing of the initial
     389                  placeholder "current_meds_AMTS"
     390          """,
     391          'current_meds_AMTS_enhanced': """emit LaTeX longtable lines with appropriate page breaks:
     392                  this returns the same content as current_meds_AMTS except that
     393                  it does not truncate output data whenever possible
     394          """,
     395
     396          'current_meds_table': "emits a LaTeX table, no arguments",
     397          'current_meds_notes': "emits a LaTeX table, no arguments",
     398          'lab_table': "emits a LaTeX table, no arguments",
     399          'test_results': "args: <%(field)s-template>//<date format>//<line separator (EOL)>",
     400          'most_recent_test_results': """most recent test results formatted as defined in <template>:
     401                  args: <dfmt=...>//<tmpl=...>//<sep=...>
     402                  <dfmt=...>: strftime format string for test result timestamps,
     403                  <tmpl=...>: target format specific %s-template, applied to each test result
     404                  <sep=...>: line separator, can be a Python string escape, such as \n
     405          """,
     406          'latest_vaccs_table': "emits a LaTeX table, no arguments",
     407          'vaccination_history': "args: <%(field)s-template//date format> to format one vaccination per line",
     408          'allergy_state': "no arguments",
     409          'allergies': "args: line template, one allergy per line",
     410          'allergy_list': "args holds: template per allergy, all allergies on one line",
     411          'problems': "args holds: line template, one problem per line",
     412          'diagnoses': 'args: line template, one diagnosis per line',
     413          'PHX': "Past medical HiXtory; args: line template//separator//strftime date format",
     414          'encounter_list': "args: per-encounter template, each ends up on one line",
     415
     416          'documents': """retrieves documents from the archive:
     417                  args:   <select>//<description>//<template>//<path template>//<path>
     418                  select: let user select which documents to include, optional, if not given: all documents included
     419                  description:    whether to include descriptions, optional
     420                  template:       something %(field)s something else (do not include '//' or '::' itself in the template)
     421                  path template:  the template for outputting the path to exported
     422                          copies of the document pages, if not given no pages are exported,
     423                          this template can contain "%(name)s" and/or "%(fullpath)s" which
     424                          is replaced by the appropriate value for each exported file
     425                  path:   into which path to export copies of the document pages, temp dir if not given""",
     426
     427          'reminders': """patient reminders:
     428                  args:   <template>//<date format>
     429                  template:       something %(field)s something else (do not include '//' or '::' itself in the template)""",
     430
     431          'external_care': """External care entries:
     432                  args:   <template>
     433                  template:       something %(field)s something else (do not include '//' or '::' itself in the template)""",
     434
     435          # provider related:
     436          'current_provider': "no arguments",
     437          'current_provider_name': """formatted name of current provider:
     438                  args: <template>,
     439                  template:       something %(field)s something else (do not include '//' or '::' itself in the template)
     440          """,
     441          'current_provider_title': """formatted name of current provider:
     442                  args: <optional template>,
     443                  template:       something %(title)s something else (do not include '//' or '::' itself in the template)
     444          """,
     445          'current_provider_firstnames': """formatted name of current provider:
     446                  args: <optional template>,
     447                  template:       something %(firstnames)s something else (do not include '//' or '::' itself in the template)
     448          """,
     449          'current_provider_lastnames': """formatted name of current provider:
     450                  args: <optional template>,
     451                  template:       something %(lastnames)s something else (do not include '//' or '::' itself in the template)
     452          """,
     453          'current_provider_external_id': "args: <type of ID>//<issuer of ID>",
     454          'primary_praxis_provider': "primary provider for current patient in this praxis",
     455          'primary_praxis_provider_external_id': "args: <type of ID>//<issuer of ID>",
     456
     457
     458          # praxis related:
     459          'praxis': """retrieve current branch of your praxis:
     460                  args: <template>//select
     461                  template:               something %(field)s something else (do not include '//' or '::' itself in the template)
     462                  select:                 if this is present allow selection of the branch rather than using the current branch""",
     463
     464          'praxis_address': "args: <optional formatting template>",
     465          'praxis_comm': "args: type//<optional formatting template>",
     466          'praxis_id': "args: <type of ID>//<issuer of ID>//<optional formatting template>",
     467          'praxis_vcf': """returns path to VCF for current praxis branch
     468                  args: <template>
     469                  template: %s-template for path
     470          """,
     471          u'praxis_mcf': u"""returns MECARD for current praxis branch
     472                  args: <format>//<template>
     473                  format: fmt=qr|mcf|txt
     474                          qr: QR code png file path,
     475                          mcf: MECARD .mcf file path,
     476                          txt: MECARD string,
     477                          default - if omitted - is "txt",
     478                  template: tmpl=<%s-template string>, "%s" if omitted
     479          """,
     480          u'praxis_scan2pay': u"""return scan2pay data or QR code for current praxis
     481                  args: <format>,
     482                          format: fmt=qr|txt
     483                                  qr: QR code png file path,
     484                                  txt: scan2pay data string,
     485                                  default - if omitted: qr
     486          """,
     487
     488          # billing related:
     489          'bill': """retrieve a bill
     490                  args: <template>//<date format>
     491                  template:               something %(field)s something else (do not include '//' or '::' itself in the template)
     492                  date format:    strftime date format""",
     493          'bill_scan2pay': u"""return scan2pay data or QR code for a bill
     494                  args: <format>,
     495                          format: fmt=qr|txt
     496                                  qr: QR code png file path,
     497                                  txt: scan2pay data string,
     498                                  default - if omitted: qr
     499          """,
     500          'bill_item': """retrieve the items of a previously retrieved (and therefore cached until the next retrieval) bill
     501                  args: <template>//<date format>
     502                  template:               something %(field)s something else (do not include '//' or '::' itself in the template)
     503                  date format:    strftime date format""",
     504          'bill_adr_street': "args: optional template (%s-style formatting template); cached per bill",
     505          'bill_adr_number': "args: optional template (%s-style formatting template); cached per bill",
     506          'bill_adr_subunit': "args: optional template (%s-style formatting template); cached per bill",
     507          'bill_adr_location': "args: optional template (%s-style formatting template); cached per bill",
     508          'bill_adr_suburb': "args: optional template (%s-style formatting template); cached per bill",
     509          'bill_adr_postcode': "args: optional template (%s-style formatting template); cached per bill",
     510          'bill_adr_region': "args: optional template (%s-style formatting template); cached per bill",
     511          'bill_adr_country': "args: optional template (%s-style formatting template); cached per bill"
     512
     513  }
     514
     515  known_variant_placeholders = __known_variant_placeholders.keys()
     516  #known_variant_placeholders.sort()
     517
     518
     519  # http://help.libreoffice.org/Common/List_of_Regular_Expressions
     520  # except that OOo cannot be non-greedy |-(
     521  #default_placeholder_regex = r'\$<.+?>\$'                               # previous working placeholder
     522          # regex logic:
     523          # starts with "$"
     524          # followed by "<"
     525          # followed by > 0 characters but NOT "<" but ONLY up to the NEXT ":"
     526          # followed by "::"
     527          # followed by any number of characters  but ONLY up to the NEXT ":"
     528          # followed by "::"
     529          # followed by any number of numbers
     530          # followed by ">"
     531          # followed by "$"
     532
     533  # previous:
     534  default_placeholder_regex = r'\$<[^<:]+::.*?::\d*?>\$|\$<[^<:]+::.*?::\d+-\d+>\$'         # this one works [except that OOo cannot be non-greedy |-(    ]
     535  first_pass_placeholder_regex = r'|'.join ([
     536          r'\$<[^<:]+::.*?(?=::\d*?>\$)::\d*?>\$',
     537          r'\$<[^<:]+::.*?(?=::\d+-\d+>\$)::\d+-\d+>\$'
     538  ])
     539  second_pass_placeholder_regex = r'|'.join ([
     540          r'\$<<[^<:]+?::.*?(?=::\d*?>>\$)::\d*?>>\$',
     541          r'\$<<[^<:]+?::.*?(?=::\d+-\d+>>\$)::\d+-\d+>>\$'
     542  ])
     543  third_pass_placeholder_regex = r'|'.join ([
     544          r'\$<<<[^<:]+?::.*?(?=::\d*?>>>\$)::\d*?>>>\$',
     545          r'\$<<<[^<:]+?::.*?(?=::\d+-\d+>>>\$)::\d+-\d+>>>\$'
     546  ])
     547
     548  default_placeholder_start = '$<'
     549  default_placeholder_end = '>$'
     550
     551  #=====================================================================
     552 -def show_placeholders():
     553
     554          fname = gmTools.get_unique_filename(prefix = 'gm-placeholders-', suffix = '.txt')
     555          ph_file = io.open(fname, mode = 'wt', encoding = 'utf8', errors = 'replace')
     556
     557          ph_file.write('Here you can find some more documentation on placeholder use:\n')
     558          ph_file.write('\n http://wiki.gnumed.de/bin/view/Gnumed/GmManualLettersForms\n\n\n')
     559
     560          ph_file.write('Variable placeholders:\n')
     561          ph_file.write('Usage: $<PLACEHOLDER_NAME::ARGUMENTS::REGION_DEFINITION>$)\n')
     562          ph_file.write(' REGION_DEFINITION:\n')
     563          ph_file.write('* a single number specifying the maximum output length or\n')
     564          ph_file.write('* a number, a "-", followed by a second number specifying the region of the string to return\n')
     565          ph_file.write('ARGUMENTS:\n')
     566          ph_file.write('* depend on the actual placeholder (see there)\n')
     567          ph_file.write('* if a template is supported it will be used to %-format the output\n')
     568          ph_file.write('* templates may be either %s-style or %(name)s-style\n')
     569          ph_file.write('* templates cannot contain "::"\n')
     570          ph_file.write('* templates cannot contain whatever the arguments divider is set to (default "//")\n')
     571          for ph in known_variant_placeholders:
     572                  txt = __known_variant_placeholders[ph]
     573                  ph_file.write('\n')
     574                  ph_file.write(' ---=== %s ===---\n' % ph)
     575                  ph_file.write('\n')
     576                  ph_file.write(txt)
     577                  ph_file.write('\n\n')
     578          ph_file.write('\n')
     579
     580          ph_file.write('Known injectable placeholders (use like: $<PLACEHOLDER_NAME::ARGUMENTS::MAX OUTPUT LENGTH>$):\n')
     581          for ph in known_injectable_placeholders:
     582                  ph_file.write(' %s\n' % ph)
     583          ph_file.write('\n')
     584
     585          ph_file.close()
     586          gmMimeLib.call_viewer_on_file(aFile = fname, block = False)
     587
     588  #=====================================================================
     589 -class gmPlaceholderHandler(gmBorg.cBorg):
     590          """Returns values for placeholders.
     591
     592          - patient related placeholders operate on the currently active patient
     593          - is passed to the forms handling code, for example
     594
     595          Return values when .debug is False:
     596          - errors with placeholders return None
     597          - placeholders failing to resolve to a value return an empty string
     598
     599          Return values when .debug is True:
     600          - errors with placeholders return an error string
     601          - placeholders failing to resolve to a value return a warning string
     602
     603          There are several types of placeholders:
     604
     605          injectable placeholders
     606                  - they must be set up before use by set_placeholder()
     607                  - they should be removed after use by unset_placeholder()
     608                  - the syntax is like extended static placeholders
     609                  - known ones are listed in known_injectable_placeholders
     610                  - per-form ones can be used but must exist before
     611                    the form is processed
     612
     613          variant placeholders
     614                  - those are listed in known_variant_placeholders
     615                  - they are parsed into placeholder, data, and maximum length
     616                  - the length is optional
     617                  - data is passed to the handler
     618
     619          Note that this cannot be called from a non-gui thread unless
     620          wrapped in wx.CallAfter().
     621          """
     622 -        def __init__(self, *args, **kwargs):
     623
     624                  self.pat = gmPerson.gmCurrentPatient()
     625                  self.debug = False
     626
     627                  self.invalid_placeholder_template = _('invalid placeholder >>>>>%s<<<<<')
     628
     629                  self.__injected_placeholders = {}
     630                  self.__cache = {}
     631
     632                  self.__esc_style = None
     633                  self.__esc_func = lambda x:x
     634
     635                  self.__ellipsis = None
     636                  self.__args_divider = '//'
     637                  self.__data_encoding = None
     638                  self.__data_encoding_strict = False
     639
     640          #--------------------------------------------------------
     641          # external API
     642          #--------------------------------------------------------
     643 -        def set_placeholder(self, key=None, value=None, known_only=True):
     644                  _log.debug('setting [%s]', key)
     645                  if key not in known_injectable_placeholders:
     646                          if known_only:
     647                                  raise ValueError('un-injectable placeholder [%s]' % key)
     648
     649                          _log.debug('placeholder [%s] not known as injectable', key)
     650
     651                  self.__injected_placeholders[key] = value
     652
     653          #--------------------------------------------------------
     654 -        def unset_placeholder(self, key=None):
     655                  _log.debug('unsetting [%s]', key)
     656                  try:
     657                          del self.__injected_placeholders[key]
     658                  except KeyError:
     659                          _log.debug('injectable placeholder [%s] unknown', key)
     660
     661          #--------------------------------------------------------
     662 -        def set_cache_value(self, key=None, value=None):
     663                  self.__cache[key] = value
     664          #--------------------------------------------------------
     665 -        def unset_cache_value(self, key=None):
     666                  del self.__cache[key]
     667
     668          #--------------------------------------------------------
     669 -        def _set_escape_style(self, escape_style=None):
     670                  self.__esc_style = escape_style
     671                  return
     672
     673          escape_style = property(lambda x:x, _set_escape_style)
     674
     675          #--------------------------------------------------------
     676 -        def _set_escape_function(self, escape_function=None):
     677                  if escape_function is None:
     678                          self.__esc_func = lambda x:x
     679                          return
     680                  if not callable(escape_function):
     681                          raise ValueError('[%s._set_escape_function]: <%s> not callable' % (self.__class__.__name__, escape_function))
     682                  self.__esc_func = escape_function
     683                  return
     684
     685          escape_function = property(lambda x:x, _set_escape_function)
     686
     687          #--------------------------------------------------------
     688 -        def _set_ellipsis(self, ellipsis):
     689                  if ellipsis == 'NONE':
     690                          ellipsis = None
     691                  self.__ellipsis = ellipsis
     692
     693          ellipsis = property(lambda x: self.__ellipsis, _set_ellipsis)
     694
     695          #--------------------------------------------------------
     696 -        def _set_arguments_divider(self, divider):
     697                  if divider == 'DEFAULT':
     698                          divider = '//'
     699                  self.__args_divider = divider
     700
     701          arguments_divider = property(lambda x: self.__args_divider, _set_arguments_divider)
     702
     703          #--------------------------------------------------------
     704 -        def _set_data_encoding(self, encoding):
     705                  if encoding == 'NONE':
     706                          self.__data_encoding = None
     707                          self.__data_encoding_strict = False
     708
     709                  self.__data_encoding_strict = False
     710                  if encoding.endswith('-strict'):
     711                          self.__data_encoding_strict = True
     712                          encoding = encoding[:-7]
     713                  try:
     714                          codecs.lookup(encoding)
     715                          self.__data_encoding = encoding
     716                  except LookupError:
     717                          _log.error('<codecs> module can NOT handle encoding [%s]' % enc)
     718
     719          data_encoding = property(lambda x: self.__data_encoding, _set_data_encoding)
     720
     721          #--------------------------------------------------------
     722          placeholder_regex = property(lambda x: default_placeholder_regex, lambda x:x)
     723
     724          first_pass_placeholder_regex = property(lambda x: first_pass_placeholder_regex, lambda x:x)
     725          second_pass_placeholder_regex = property(lambda x: second_pass_placeholder_regex, lambda x:x)
     726          third_pass_placeholder_regex = property(lambda x: third_pass_placeholder_regex, lambda x:x)
     727
     728          #--------------------------------------------------------
     729 -        def __parse_region_definition(self, region_str):
     730                  region_str = region_str.strip()
     731
     732                  if region_str == '':
     733                          return None, None
     734
     735                  try:
     736                          pos_last_char = int(region_str)
     737                          return 0, pos_last_char
     738                  except (TypeError, ValueError):
     739                          _log.debug('region definition not a simple length')
     740
     741                  # note that we only check for "legality", not for reasonable bounds
     742                  first_last = region_str.split('-')
     743                  if len(first_last) != 2:
     744                          _log.error('invalid placeholder region definition: %s', region_str)
     745                          raise ValueError
     746
     747                  try:
     748                          pos_first_char = int(first_last[0].strip())
     749                          pos_last_char = int(first_last[1].strip())
     750                  except (TypeError, ValueError):
     751                          _log.error('invalid placeholder region definition: %s', region_str)
     752                          raise ValueError
     753
     754                  # user says 1,2,... (= character position in string), Python needs 0,1,... (indexes 0-based)
     755                  if pos_first_char > 0:
     756                          pos_first_char -= 1
     757
     758                  return pos_first_char, pos_last_char
     759
     760          #--------------------------------------------------------
     761 -        def __make_compatible_with_encoding(self, data_str):
     762                  if self.__data_encoding is None:
     763                          return data_str
     764
     765                  try:
     766                          codecs.encode(data_str, self.__data_encoding, 'strict')
     767                          return data_str
     768                  except UnicodeEncodeError:
     769                          _log.error('cannot strict-encode string into [%s]: %s', self.__data_encoding, data_str)
     770
     771                  if self.__data_encoding_strict:
     772                          return 'not compatible with encoding [%s]: %s' % (self.__data_encoding, data_str)
     773
     774                  try:
     775                          import unidecode
     776                  except ImportError:
     777                          _log.debug('cannot transliterate, <unidecode> module not installed')
     778                          return codecs.encode(data_str, self.__data_encoding, 'replace').decode(self.__data_encoding)
     779
     780                  return unidecode.unidecode(data_str).decode('utf8')
     781
     782          #--------------------------------------------------------
     783          # __getitem__ API
     784          #--------------------------------------------------------
     785 -        def __getitem__(self, placeholder):
     786                  """Map self['placeholder'] to self.placeholder.
     787
     788                  This is useful for replacing placeholders parsed out
     789                  of documents as strings.
     790
     791                  Unknown/invalid placeholders still deliver a result but
     792                  it will be glaringly obvious if debugging is enabled.
     793                  """
     794                  _log.debug('replacing [%s]', placeholder)
     795
     796                  original_placeholder_def = placeholder
     797
     798                  # remove leading/trailing '$<(<<)' and '(>>)>$'
     799                  if placeholder.startswith(default_placeholder_start):
     800                          placeholder = placeholder.lstrip('$').lstrip('<')
     801                          if placeholder.endswith(default_placeholder_end):
     802                                  placeholder = placeholder.rstrip('$').rstrip('>')
     803                          else:
     804                                  _log.error('placeholder must either start with [%s] and end with [%s] or neither of both', default_placeholder_start, default_placeholder_end)
     805                                  if self.debug:
     806                                          return self._escape(self.invalid_placeholder_template % original_placeholder_def)
     807                                  return None
     808
     809                  # injectable placeholder ?
     810                  parts = placeholder.split('::::', 1)
     811                  if len(parts) == 2:
     812                          ph_name, region_str = parts
     813                          is_an_injectable = True
     814                          try:
     815                                  val = self.__injected_placeholders[ph_name]
     816                          except KeyError:
     817                                  is_an_injectable = False
     818                          except Exception:
     819                                  _log.exception('injectable placeholder handling error: %s', original_placeholder_def)
     820                                  if self.debug:
     821                                          return self._escape(self.invalid_placeholder_template % original_placeholder_def)
     822
     823                                  return None
     824
     825                          if is_an_injectable:
     826                                  if val is None:
     827                                          if self.debug:
     828                                                  return self._escape('injectable placeholder [%s]: no value available' % ph_name)
     829                                          return placeholder
     830                                  try:
     831                                          pos_first_char, pos_last_char = self.__parse_region_definition(region_str)
     832                                  except ValueError:
     833                                          if self.debug:
     834                                                  return self._escape(self.invalid_placeholder_template % original_placeholder_def)
     835                                          return None
     836                                  if pos_last_char is None:
     837                                          return self.__make_compatible_with_encoding(val)
     838                                  # ellipsis needed ?
     839                                  if len(val) > (pos_last_char - pos_first_char):
     840                                          # ellipsis wanted ?
     841                                          if self.__ellipsis is not None:
     842                                                  return self.__make_compatible_with_encoding(val[pos_first_char:(pos_last_char-len(self.__ellipsis))] + self.__ellipsis)
     843                                  return self.__make_compatible_with_encoding(val[pos_first_char:pos_last_char])
     844
     845                  # variable placeholders
     846                  if len(placeholder.split('::', 2)) < 3:
     847                          _log.error('invalid placeholder structure: %s', original_placeholder_def)
     848                          if self.debug:
     849                                  return self._escape(self.invalid_placeholder_template % original_placeholder_def)
     850                          return None
     851
     852                  ph_name, data_and_lng = placeholder.split('::', 1)              # note: split _is_ lsplit
     853                  options, region_str = data_and_lng.rsplit('::', 1)
     854                  _log.debug('placeholder parts: name=[%s]; region_def=[%s]; options=>>>%s<<<', ph_name, region_str, options)
     855                  try:
     856                          pos_first_char, pos_last_char = self.__parse_region_definition(region_str)
     857                  except ValueError:
     858                          if self.debug:
     859                                  return self._escape(self.invalid_placeholder_template % original_placeholder_def)
     860                          return None
     861
     862                  handler = getattr(self, '_get_variant_%s' % ph_name, None)
     863                  if handler is None:
     864                          _log.warning('no handler <_get_variant_%s> for placeholder %s', ph_name, original_placeholder_def)
     865                          if self.debug:
     866                                  return self._escape(self.invalid_placeholder_template % original_placeholder_def)
     867                          return None
     868
     869                  try:
     870                          val = handler(data = options)
     871                          if pos_last_char is None:
     872                                  return self.__make_compatible_with_encoding(val)
     873                          # ellipsis needed ?
     874                          if len(val) > (pos_last_char - pos_first_char):
     875                                  # ellipsis wanted ?
     876                                  if self.__ellipsis is not None:
     877                                          return self.__make_compatible_with_encoding(val[pos_first_char:(pos_last_char-len(self.__ellipsis))] + self.__ellipsis)
     878                          return self.__make_compatible_with_encoding(val[pos_first_char:pos_last_char])
     879
     880                  except Exception:
     881                          _log.exception('placeholder handling error: %s', original_placeholder_def)
     882                          if self.debug:
     883                                  return self._escape(self.invalid_placeholder_template % original_placeholder_def)
     884                          return None
     885
     886                  _log.error('something went wrong, should never get here')
     887                  return None
     888
     889          #--------------------------------------------------------
     890          # placeholder handlers
     891          #--------------------------------------------------------
     892 -        def _get_variant_ph_cfg(self, data=None):
     893                  options = data.split('//')              # ALWAYS use '//' for splitting, regardless of self.__args_divider
     894                  name = options[0]
     895                  val = options[1]
     896                  if name == 'ellipsis':
     897                          self.ellipsis = val
     898                  elif name == 'argumentsdivider':
     899                          self.arguments_divider = val
     900                  elif name == 'encoding':
     901                          self.data_encoding = val
     902                  if len(options) > 2:
     903                          return options[2] % {'name': name, 'value': val}
     904                  return ''
     905          #--------------------------------------------------------
     906 -        def _get_variant_client_version(self, data=None):
     907                  return self._escape (
     908                          gmTools.coalesce (
     909                                  _cfg.get(option = 'client_version'),
     910                                  '%s' % self.__class__.__name__
     911                          )
     912                  )
     913          #--------------------------------------------------------
     914 -        def _get_variant_reminders(self, data=None):
     915
     916                  from Gnumed.wxpython import gmProviderInboxWidgets
     917
     918                  template = _('due %(due_date)s: %(comment)s (%(interval_due)s)')
     919                  date_format = '%Y %b %d'
     920
     921                  data_parts = data.split(self.__args_divider)
     922
     923                  if len(data_parts) > 0:
     924                          if data_parts[0].strip() != '':
     925                                  template = data_parts[0]
     926
     927                  if len(data_parts) > 1:
     928                          if data_parts[1].strip() != '':
     929                                  date_format = data_parts[1]
     930
     931                  reminders = gmProviderInboxWidgets.manage_reminders(patient = self.pat.ID)
     932
     933                  if reminders is None:
     934                          return ''
     935
     936                  if len(reminders) == 0:
     937                          return ''
     938
     939                  lines = [ template % r.fields_as_dict(date_format = date_format, escape_style = self.__esc_style) for r in reminders ]
     940
     941                  return '\n'.join(lines)
     942          #--------------------------------------------------------
     943 -        def _get_variant_external_care(self, data=None):
     944
     945                  from Gnumed.wxpython import gmExternalCareWidgets
     946                  external_cares = gmExternalCareWidgets.manage_external_care()
     947
     948                  if external_cares is None:
     949                          return ''
     950
     951                  if len(external_cares) == 0:
     952                          return ''
     953
     954                  template = data
     955                  lines = [ template % ext.fields_as_dict(escape_style = self.__esc_style) for ext in external_cares ]
     956
     957                  return '\n'.join(lines)
     958          #--------------------------------------------------------
     959 -        def _get_variant_documents(self, data=None):
     960
     961                  select = False
     962                  include_descriptions = False
     963                  template = '%s'
     964                  path_template = None
     965                  export_path = None
     966
     967                  data_parts = data.split(self.__args_divider)
     968
     969                  if 'select' in data_parts:
     970                          select = True
     971                          data_parts.remove('select')
     972
     973                  if 'description' in data_parts:
     974                          include_descriptions = True
     975                          data_parts.remove('description')
     976
     977                  template = data_parts[0]
     978
     979                  if len(data_parts) > 1:
     980                          path_template = data_parts[1]
     981
     982                  if len(data_parts) > 2:
     983                          export_path = data_parts[2]
     984
     985                  # create path
     986                  if export_path is not None:
     987                          export_path = os.path.normcase(os.path.expanduser(export_path))
     988                          gmTools.mkdir(export_path)
     989
     990                  # select docs
     991                  if select:
     992                          docs = gmDocumentWidgets.manage_documents(msg = _('Select the patient documents to reference from the new document.'), single_selection = False)
     993                  else:
     994                          docs = self.pat.document_folder.documents
     995
     996                  if docs is None:
     997                          return ''
     998
     999                  lines = []
    1000                  for doc in docs:
    1001                          lines.append(template % doc.fields_as_dict(date_format = '%Y %b %d', escape_style = self.__esc_style))
    1002                          if include_descriptions:
    1003                                  for desc in doc.get_descriptions(max_lng = None):
    1004                                          lines.append(self._escape(desc['text'] + '\n'))
    1005                          if path_template is not None:
    1006                                  for part_name in doc.save_parts_to_files(export_dir = export_path):
    1007                                          path, name = os.path.split(part_name)
    1008                                          lines.append(path_template % {'fullpath': part_name, 'name': name})
    1009
    1010                  return '\n'.join(lines)
    1011          #--------------------------------------------------------
    1012 -        def _get_variant_encounter_list(self, data=None):
    1013
    1014                  encounters = gmEncounterWidgets.select_encounters(single_selection = False)
    1015                  if not encounters:
    1016                          return ''
    1017
    1018                  template = data
    1019
    1020                  lines = []
    1021                  for enc in encounters:
    1022                          try:
    1023                                  lines.append(template % enc.fields_as_dict(date_format = '%Y %b %d', escape_style = self.__esc_style))
    1024                          except Exception:
    1025                                  lines.append('error formatting encounter')
    1026                                  _log.exception('problem formatting encounter list')
    1027                                  _log.error('template: %s', template)
    1028                                  _log.error('encounter: %s', encounter)
    1029
    1030                  return '\n'.join(lines)
    1031          #--------------------------------------------------------
    1032 -        def _get_variant_soap_for_encounters(self, data=None):
    1033                  """Select encounters from list and format SOAP thereof.
    1034
    1035                  data: soap_cats (' ' -> None -> admin) // date format
    1036                  """
    1037                  # defaults
    1038                  cats = None
    1039                  date_format = None
    1040
    1041                  if data is not None:
    1042                          data_parts = data.split(self.__args_divider)
    1043
    1044                          # part[0]: categories
    1045                          if len(data_parts[0]) > 0:
    1046                                  cats = []
    1047                                  if ' ' in data_parts[0]:
    1048                                          cats.append(None)
    1049                                          data_parts[0] = data_parts[0].replace(' ', '')
    1050                                  cats.extend(list(data_parts[0]))
    1051
    1052                          # part[1]: date format
    1053                          if len(data_parts) > 1:
    1054                                  if len(data_parts[1]) > 0:
    1055                                          date_format = data_parts[1]
    1056
    1057                  encounters = gmEncounterWidgets.select_encounters(single_selection = False)
    1058                  if not encounters:
    1059                          return ''
    1060
    1061                  chunks = []
    1062                  for enc in encounters:
    1063                          chunks.append(enc.format_latex (
    1064                                  date_format = date_format,
    1065                                  soap_cats = cats,
    1066                                  soap_order = 'soap_rank, date'
    1067                          ))
    1068
    1069                  return ''.join(chunks)
    1070          #--------------------------------------------------------
    1071 -        def _get_variant_emr_journal(self, data=None):
    1072                  # default: all categories, neutral template
    1073                  cats = list('soapu')
    1074                  cats.append(None)
    1075                  template = '%s'
    1076                  interactive = True
    1077                  line_length = 9999
    1078                  time_range = None
    1079
    1080                  if data is not None:
    1081                          data_parts = data.split(self.__args_divider)
    1082
    1083                          # part[0]: categories
    1084                          cats = []
    1085                          # ' ' -> None == admin
    1086                          for c in list(data_parts[0]):
    1087                                  if c == ' ':
    1088                                          c = None
    1089                                  cats.append(c)
    1090                          # '' -> SOAP + None
    1091                          if cats == '':
    1092                                  cats = list('soapu').append(None)
    1093
    1094                          # part[1]: template
    1095                          if len(data_parts) > 1:
    1096                                  template = data_parts[1]
    1097
    1098                          # part[2]: line length
    1099                          if len(data_parts) > 2:
    1100                                  try:
    1101                                          line_length = int(data_parts[2])
    1102                                  except Exception:
    1103                                          line_length = 9999
    1104
    1105                          # part[3]: weeks going back in time
    1106                          if len(data_parts) > 3:
    1107                                  try:
    1108                                          time_range = 7 * int(data_parts[3])
    1109                                  except Exception:
    1110                                          #time_range = None                      # infinite
    1111                                          # pass on literally, meaning it must be a valid PG interval string
    1112                                          time_range = data_parts[3]
    1113
    1114                  # FIXME: will need to be a generator later on
    1115                  narr = self.pat.emr.get_as_journal(soap_cats = cats, time_range = time_range)
    1116
    1117                  if len(narr) == 0:
    1118                          return ''
    1119
    1120                  keys = narr[0].keys()
    1121                  lines = []
    1122                  line_dict = {}
    1123                  for n in narr:
    1124                          for key in keys:
    1125                                  if isinstance(n[key], str):
    1126                                          line_dict[key] = self._escape(text = n[key])
    1127                                          continue
    1128                                  line_dict[key] = n[key]
    1129                          try:
    1130                                  lines.append((template % line_dict)[:line_length])
    1131                          except KeyError:
    1132                                  return 'invalid key in template [%s], valid keys: %s]' % (template, str(keys))
    1133
    1134                  return '\n'.join(lines)
    1135          #--------------------------------------------------------
    1136 -        def _get_variant_soap_by_issue(self, data=None):
    1137                  return self.__get_variant_soap_by_issue_or_episode(data = data, mode = 'issue')
    1138          #--------------------------------------------------------
    1139 -        def _get_variant_soap_by_episode(self, data=None):
    1140                  return self.__get_variant_soap_by_issue_or_episode(data = data, mode = 'episode')
    1141          #--------------------------------------------------------
    1142 -        def __get_variant_soap_by_issue_or_episode(self, data=None, mode=None):
    1143
    1144                  # default: all categories, neutral template
    1145                  cats = list('soapu')
    1146                  cats.append(None)
    1147
    1148                  date_format = None
    1149                  template = '%s'
    1150
    1151                  if data is not None:
    1152                          data_parts = data.split(self.__args_divider)
    1153
    1154                          # part[0]: categories
    1155                          if len(data_parts[0]) > 0:
    1156                                  cats = []
    1157                                  if ' ' in data_parts[0]:
    1158                                          cats.append(None)
    1159                                  cats.extend(list(data_parts[0].replace(' ', '')))
    1160
    1161                          # part[1]: date format
    1162                          if len(data_parts) > 1:
    1163                                  if len(data_parts[1]) > 0:
    1164                                          date_format = data_parts[1]
    1165
    1166                          # part[2]: template
    1167                          if len(data_parts) > 2:
    1168                                  if len(data_parts[2]) > 0:
    1169                                          template = data_parts[2]
    1170
    1171                  if mode == 'issue':
    1172                          narr = gmNarrativeWorkflows.select_narrative_by_issue(soap_cats = cats)
    1173                  else:
    1174                          narr = gmNarrativeWorkflows.select_narrative_by_episode(soap_cats = cats)
    1175
    1176                  if narr is None:
    1177                          return ''
    1178
    1179                  if len(narr) == 0:
    1180                          return ''
    1181
    1182                  try:
    1183                          narr = [ template % n.fields_as_dict(date_format = date_format, escape_style = self.__esc_style) for n in narr ]
    1184                  except KeyError:
    1185                          return 'invalid key in template [%s], valid keys: %s]' % (template, str(narr[0].keys()))
    1186
    1187                  return '\n'.join(narr)
    1188          #--------------------------------------------------------
    1189 -        def _get_variant_progress_notes(self, data=None):
    1190                  return self._get_variant_soap(data = data)
    1191          #--------------------------------------------------------
    1192 -        def _get_variant_soap_s(self, data=None):
    1193                  return self._get_variant_soap(data = 's')
    1194          #--------------------------------------------------------
    1195 -        def _get_variant_soap_o(self, data=None):
    1196                  return self._get_variant_soap(data = 'o')
    1197          #--------------------------------------------------------
    1198 -        def _get_variant_soap_a(self, data=None):
    1199                  return self._get_variant_soap(data = 'a')
    1200          #--------------------------------------------------------
    1201 -        def _get_variant_soap_p(self, data=None):
    1202                  return self._get_variant_soap(data = 'p')
    1203          #--------------------------------------------------------
    1204 -        def _get_variant_soap_u(self, data=None):
    1205                  return self._get_variant_soap(data = 'u')
    1206          #--------------------------------------------------------
    1207 -        def _get_variant_soap_admin(self, data=None):
    1208                  return self._get_variant_soap(data = ' ')
    1209          #--------------------------------------------------------
    1210 -        def _get_variant_soap(self, data=None):
    1211
    1212                  # default: all categories, neutral template
    1213                  cats = list('soapu')
    1214                  cats.append(None)
    1215                  template = '%(narrative)s'
    1216
    1217                  if data is not None:
    1218                          data_parts = data.split(self.__args_divider)
    1219
    1220                          # part[0]: categories
    1221                          cats = []
    1222                          # ' ' -> None == admin
    1223                          for cat in list(data_parts[0]):
    1224                                  if cat == ' ':
    1225                                          cat = None
    1226                                  cats.append(cat)
    1227                          # '' -> SOAP + None
    1228                          if cats == '':
    1229                                  cats = list('soapu')
    1230                                  cats.append(None)
    1231
    1232                          # part[1]: template
    1233                          if len(data_parts) > 1:
    1234                                  template = data_parts[1]
    1235
    1236                  #narr = gmNarrativeWorkflows.select_narrative_from_episodes(soap_cats = cats)
    1237                  narr = gmNarrativeWorkflows.select_narrative(soap_cats = cats)
    1238
    1239                  if narr is None:
    1240                          return ''
    1241
    1242                  if len(narr) == 0:
    1243                          return ''
    1244
    1245                  # if any "%s" is in the template there cannot be any %(field)s
    1246                  # and we also restrict the fields to .narrative (this is the
    1247                  # old placeholder behaviour
    1248                  if '%s' in template:
    1249                          narr = [ self._escape(n['narrative']) for n in narr ]
    1250                  else:
    1251                          narr = [ n.fields_as_dict(escape_style = self.__esc_style) for n in narr ]
    1252
    1253                  try:
    1254                          narr = [ template % n for n in narr ]
    1255                  except KeyError:
    1256                          return 'invalid key in template [%s], valid keys: %s]' % (template, str(narr[0].keys()))
    1257                  except TypeError:
    1258                          return 'cannot mix "%%s" and "%%(field)s" in template [%s]' % template
    1259
    1260                  return '\n'.join(narr)
    1261
    1262          #--------------------------------------------------------
    1263 -        def _get_variant_title(self, data=None):
    1264                  return self._get_variant_name(data = '%(title)s')
    1265          #--------------------------------------------------------
    1266 -        def _get_variant_firstname(self, data=None):
    1267                  return self._get_variant_name(data = '%(firstnames)s')
    1268          #--------------------------------------------------------
    1269 -        def _get_variant_lastname(self, data=None):
    1270                  return self._get_variant_name(data = '%(lastnames)s')
    1271          #--------------------------------------------------------
    1272 -        def _get_variant_name(self, data=None):
    1273                  if data is None:
    1274                          return [_('template is missing')]
    1275
    1276                  name = self.pat.get_active_name()
    1277
    1278                  parts = {
    1279                          'title': self._escape(gmTools.coalesce(name['title'], '')),
    1280                          'firstnames': self._escape(name['firstnames']),
    1281                          'lastnames': self._escape(name['lastnames']),
    1282                          'preferred': self._escape(gmTools.coalesce (
    1283                                  value2test = name['preferred'],
    1284                                  return_instead = ' ',
    1285                                  template4value = ' "%s" '
    1286                          ))
    1287                  }
    1288
    1289                  return data % parts
    1290
    1291          #--------------------------------------------------------
    1292 -        def _get_variant_date_of_birth(self, data='%Y %b %d'):
    1293                  return self.pat.get_formatted_dob(format = data)
    1294
    1295          #--------------------------------------------------------
    1296          # FIXME: extend to all supported genders
    1297 -        def _get_variant_gender_mapper(self, data='male//female//other'):
    1298
    1299                  values = data.split('//', 2)
    1300
    1301                  if len(values) == 2:
    1302                          male_value, female_value = values
    1303                          other_value = '<unkown gender>'
    1304                  elif len(values) == 3:
    1305                          male_value, female_value, other_value = values
    1306                  else:
    1307                          return _('invalid gender mapping layout: [%s]') % data
    1308
    1309                  if self.pat['gender'] == 'm':
    1310                          return self._escape(male_value)
    1311
    1312                  if self.pat['gender'] == 'f':
    1313                          return self._escape(female_value)
    1314
    1315                  return self._escape(other_value)
    1316          #--------------------------------------------------------
    1317          # address related placeholders
    1318          #--------------------------------------------------------
    1319 -        def __get_variant_gen_adr_part(self, data='?', part=None):
    1320
    1321                  template = '%s'
    1322                  msg = _('Select the address you want to use !')
    1323                  cache_id = ''
    1324                  options = data.split('//', 4)
    1325                  if len(options) > 0:
    1326                          template = options[0]
    1327                          if template.strip() == '':
    1328                                  template = '%s'
    1329                  if len(options) > 1:
    1330                          msg = options[1]
    1331                  if len(options) > 2:
    1332                          cache_id = options[2]
    1333
    1334                  cache_key = 'generic_address::' + cache_id
    1335                  try:
    1336                          adr2use = self.__cache[cache_key]
    1337                          _log.debug('cache hit (%s): [%s]', cache_key, adr2use)
    1338                  except KeyError:
    1339                          adr2use = None
    1340
    1341                  if adr2use is None:
    1342                          dlg = gmAddressWidgets.cAddressSelectionDlg(None, -1)
    1343                          dlg.message = msg
    1344                          choice = dlg.ShowModal()
    1345                          adr2use = dlg.address
    1346                          dlg.DestroyLater()
    1347                          if choice == wx.ID_CANCEL:
    1348                                  return ''
    1349                          self.__cache[cache_key] = adr2use
    1350
    1351                  return template % self._escape(adr2use[part])
    1352          #--------------------------------------------------------
    1353 -        def _get_variant_gen_adr_street(self, data='?'):
    1354                  return self.__get_variant_gen_adr_part(data = data, part = 'street')
    1355          #--------------------------------------------------------
    1356 -        def _get_variant_gen_adr_number(self, data='?'):
    1357                  return self.__get_variant_gen_adr_part(data = data, part = 'number')
    1358          #--------------------------------------------------------
    1359 -        def _get_variant_gen_adr_subunit(self, data='?'):
    1360                  return self.__get_variant_gen_adr_part(data = data, part = 'subunit')
    1361          #--------------------------------------------------------
    1362 -        def _get_variant_gen_adr_location(self, data='?'):
    1363                  return self.__get_variant_gen_adr_part(data = data, part = 'urb')
    1364          #--------------------------------------------------------
    1365 -        def _get_variant_gen_adr_suburb(self, data='?'):
    1366                  return self.__get_variant_gen_adr_part(data = data, part = 'suburb')
    1367          #--------------------------------------------------------
    1368 -        def _get_variant_gen_adr_postcode(self, data='?'):
    1369                  return self.__get_variant_gen_adr_part(data = data, part = 'postcode')
    1370          #--------------------------------------------------------
    1371 -        def _get_variant_gen_adr_region(self, data='?'):
    1372                  return self.__get_variant_gen_adr_part(data = data, part = 'l10n_region')
    1373          #--------------------------------------------------------
    1374 -        def _get_variant_gen_adr_country(self, data='?'):
    1375                  return self.__get_variant_gen_adr_part(data = data, part = 'l10n_country')
    1376          #--------------------------------------------------------
    1377 -        def __get_variant_receiver_part(self, data='%s', part=None):
    1378
    1379                  template = '%s'
    1380                  cache_id = ''
    1381                  options = data.split('//', 3)
    1382                  if len(options) > 0:
    1383                          template = options[0]
    1384                          if template.strip() == '':
    1385                                  template = '%s'
    1386                  if len(options) > 1:
    1387                          cache_id = options[1]
    1388
    1389                  cache_key = 'receiver::' + cache_id
    1390                  try:
    1391                          name, adr = self.__cache[cache_key]
    1392                          _log.debug('cache hit (%s): [%s:%s]', cache_key, name, adr)
    1393                  except KeyError:
    1394                          name = None
    1395                          adr = None
    1396
    1397                  if name is None:
    1398                          from Gnumed.wxpython import gmFormWidgets
    1399                          dlg = gmFormWidgets.cReceiverSelectionDlg(None, -1)
    1400                          dlg.patient = self.pat
    1401                          choice = dlg.ShowModal()
    1402                          name = dlg.name
    1403                          adr = dlg.address
    1404                          dlg.DestroyLater()
    1405                          if choice == wx.ID_CANCEL:
    1406                                  return ''
    1407                          self.__cache[cache_key] = (name, adr)
    1408
    1409                  if part == 'name':
    1410                          return template % self._escape(name)
    1411
    1412                  return template % self._escape(gmTools.coalesce(adr[part], ''))
    1413          #--------------------------------------------------------
    1414 -        def _get_variant_receiver_name(self, data='%s'):
    1415                  return self.__get_variant_receiver_part(data = data, part = 'name')
    1416          #--------------------------------------------------------
    1417 -        def _get_variant_receiver_street(self, data='%s'):
    1418                  return self.__get_variant_receiver_part(data = data, part = 'street')
    1419          #--------------------------------------------------------
    1420 -        def _get_variant_receiver_number(self, data='%s'):
    1421                  return self.__get_variant_receiver_part(data = data, part = 'number')
    1422          #--------------------------------------------------------
    1423 -        def _get_variant_receiver_subunit(self, data='%s'):
    1424                  return self.__get_variant_receiver_part(data = data, part = 'subunit')
    1425          #--------------------------------------------------------
    1426 -        def _get_variant_receiver_location(self, data='%s'):
    1427                  return self.__get_variant_receiver_part(data = data, part = 'urb')
    1428          #--------------------------------------------------------
    1429 -        def _get_variant_receiver_suburb(self, data='%s'):
    1430                  return self.__get_variant_receiver_part(data = data, part = 'suburb')
    1431          #--------------------------------------------------------
    1432 -        def _get_variant_receiver_postcode(self, data='%s'):
    1433                  return self.__get_variant_receiver_part(data = data, part = 'postcode')
    1434          #--------------------------------------------------------
    1435 -        def _get_variant_receiver_region(self, data='%s'):
    1436                  return self.__get_variant_receiver_part(data = data, part = 'l10n_region')
    1437          #--------------------------------------------------------
    1438 -        def _get_variant_receiver_country(self, data='%s'):
    1439                  return self.__get_variant_receiver_part(data = data, part = 'l10n_country')
    1440          #--------------------------------------------------------
    1441 -        def _get_variant_patient_address(self, data=''):
    1442
    1443                  data_parts = data.split(self.__args_divider)
    1444
    1445                  # address type
    1446                  adr_type = data_parts[0].strip()
    1447                  orig_type = adr_type
    1448                  if adr_type != '':
    1449                          adrs = self.pat.get_addresses(address_type = adr_type)
    1450                          if len(adrs) == 0:
    1451                                  _log.warning('no address for type [%s]', adr_type)
    1452                                  adr_type = ''
    1453                  if adr_type == '':
    1454                          _log.debug('asking user for address type')
    1455                          adr = gmPersonContactWidgets.select_address(missing = orig_type, person = self.pat)
    1456                          if adr is None:
    1457                                  if self.debug:
    1458                                          return _('no address type replacement selected')
    1459                                  return ''
    1460                          adr_type = adr['address_type']
    1461                  adr = self.pat.get_addresses(address_type = adr_type)[0]
    1462
    1463                  # formatting template
    1464                  template = _('%(street)s %(number)s, %(postcode)s %(urb)s, %(l10n_region)s, %(l10n_country)s')
    1465                  if len(data_parts) > 1:
    1466                          if data_parts[1].strip() != '':
    1467                                  template = data_parts[1]
    1468
    1469                  try:
    1470                          return template % adr.fields_as_dict(escape_style = self.__esc_style)
    1471                  except Exception:
    1472                          _log.exception('error formatting address')
    1473                          _log.error('template: %s', template)
    1474
    1475                  return None
    1476          #--------------------------------------------------------
    1477 -        def __get_variant_adr_part(self, data='?', part=None):
    1478                  requested_type = data.strip()
    1479                  cache_key = 'adr-type-%s' % requested_type
    1480                  try:
    1481                          type2use = self.__cache[cache_key]
    1482                          _log.debug('cache hit (%s): [%s] -> [%s]', cache_key, requested_type, type2use)
    1483                  except KeyError:
    1484                          type2use = requested_type
    1485                          if type2use != '':
    1486                                  adrs = self.pat.get_addresses(address_type = type2use)
    1487                                  if len(adrs) == 0:
    1488                                          _log.warning('no address of type [%s] for <%s> field extraction', requested_type, part)
    1489                                          type2use = ''
    1490                          if type2use == '':
    1491                                  _log.debug('asking user for replacement address type')
    1492                                  adr = gmPersonContactWidgets.select_address(missing = requested_type, person = self.pat)
    1493                                  if adr is None:
    1494                                          _log.debug('no replacement selected')
    1495                                          if self.debug:
    1496                                                  return self._escape(_('no address type replacement selected'))
    1497                                          return ''
    1498                                  type2use = adr['address_type']
    1499                                  self.__cache[cache_key] = type2use
    1500                                  _log.debug('caching (%s): [%s] -> [%s]', cache_key, requested_type, type2use)
    1501
    1502                  part_data = self.pat.get_addresses(address_type = type2use)[0][part]
    1503                  if part_data is None:
    1504                          part_data = ''  # do escape empty string since we never know what target formats need
    1505                  return self._escape(part_data)
    1506
    1507          #--------------------------------------------------------
    1508 -        def _get_variant_adr_street(self, data='?'):
    1509                  return self.__get_variant_adr_part(data = data, part = 'street')
    1510          #--------------------------------------------------------
    1511 -        def _get_variant_adr_number(self, data='?'):
    1512                  return self.__get_variant_adr_part(data = data, part = 'number')
    1513          #--------------------------------------------------------
    1514 -        def _get_variant_adr_subunit(self, data='?'):
    1515                  return self.__get_variant_adr_part(data = data, part = 'subunit')
    1516          #--------------------------------------------------------
    1517 -        def _get_variant_adr_location(self, data='?'):
    1518                  return self.__get_variant_adr_part(data = data, part = 'urb')
    1519          #--------------------------------------------------------
    1520 -        def _get_variant_adr_suburb(self, data='?'):
    1521                  return self.__get_variant_adr_part(data = data, part = 'suburb')
    1522          #--------------------------------------------------------
    1523 -        def _get_variant_adr_postcode(self, data='?'):
    1524                  return self.__get_variant_adr_part(data = data, part = 'postcode')
    1525          #--------------------------------------------------------
    1526 -        def _get_variant_adr_region(self, data='?'):
    1527                  return self.__get_variant_adr_part(data = data, part = 'l10n_region')
    1528          #--------------------------------------------------------
    1529 -        def _get_variant_adr_country(self, data='?'):
    1530                  return self.__get_variant_adr_part(data = data, part = 'l10n_country')
    1531          #--------------------------------------------------------
    1532 -        def _get_variant_patient_comm(self, data=None):
    1533                  comm_type = None
    1534                  template = '%(url)s'
    1535                  if data is not None:
    1536                          data_parts = data.split(self.__args_divider)
    1537                          if len(data_parts) > 0:
    1538                                  comm_type = data_parts[0]
    1539                          if len(data_parts) > 1:
    1540                                  template = data_parts[1]
    1541
    1542                  comms = self.pat.get_comm_channels(comm_medium = comm_type)
    1543                  if len(comms) == 0:
    1544                          if self.debug:
    1545                                  return self._escape(_('no URL for comm channel [%s]') % data)
    1546                          return ''
    1547
    1548                  return template % comms[0].fields_as_dict(escape_style = self.__esc_style)
    1549          #--------------------------------------------------------
    1550 -        def _get_variant_patient_photo(self, data=None):
    1551
    1552                  template = '%s'
    1553                  target_mime = None
    1554                  target_ext = None
    1555                  if data is not None:
    1556                          parts = data.split(self.__args_divider)
    1557                          template = parts[0]
    1558                          if len(parts) > 1:
    1559                                  target_mime = parts[1].strip()
    1560                          if len(parts) > 2:
    1561                                  target_ext = parts[2].strip()
    1562                          if target_ext is None:
    1563                                  if target_mime is not None:
    1564                                          target_ext = gmMimeLib.guess_ext_by_mimetype(mimetype = target_mime)
    1565
    1566                  cache_key = 'patient_photo_path::%s::%s' % (target_mime, target_ext)
    1567                  try:
    1568                          fname = self.__cache[cache_key]
    1569                          _log.debug('cache hit on [%s]: %s', cache_key, fname)
    1570                  except KeyError:
    1571                          mugshot = self.pat.document_folder.latest_mugshot
    1572                          if mugshot is None:
    1573                                  if self.debug:
    1574                                          return self._escape(_('no mugshot available'))
    1575                                  return ''
    1576                          fname = mugshot.save_to_file (
    1577                                  target_mime = target_mime,
    1578                                  target_extension = target_ext,
    1579                                  ignore_conversion_problems = True
    1580                          )
    1581                          if fname is None:
    1582                                  if self.debug:
    1583                                          return self._escape(_('cannot export or convert latest mugshot'))
    1584                                  return ''
    1585                          self.__cache[cache_key] = fname
    1586
    1587                  return template % fname
    1588
    1589          #--------------------------------------------------------
    1590 -        def _get_variant_patient_vcf(self, data):
    1591                  options = data.split(self.__args_divider)
    1592                  template = options[0].strip()
    1593                  if template == '':
    1594                          template = '%s'
    1595
    1596                  return template % self.pat.export_as_vcard()
    1597
    1598          #--------------------------------------------------------
    1599 -        def _get_variant_patient_mcf(self, data):
    1600                  template, format = self.__parse_ph_options (
    1601                          option_defs = {'tmpl': '%s', 'fmt': 'txt'},
    1602                          options_string = data
    1603                  )
    1604                  if format not in ['qr', 'mcf', 'txt']:
    1605                          if self.debug:
    1606                                  return self._escape(_('patient_mcf: invalid format (qr/mcf/txt)'))
    1607                          return ''
    1608
    1609                  if format == 'txt':
    1610                          return template % self._escape(self.pat.MECARD)
    1611
    1612                  if format == 'mcf':
    1613                          return template % self.pat.export_as_mecard()
    1614
    1615                  if format == 'qr':
    1616                          qr_filename = gmTools.create_qrcode(text = self.pat.MECARD)
    1617                          if qr_filename is None:
    1618                                  return self._escape('patient_mcf-cannot_create_QR_code')
    1619                          return template % qr_filename
    1620
    1621                  return None
    1622
    1623          #--------------------------------------------------------
    1624 -        def _get_variant_patient_gdt(self, data):
    1625                  options = data.split(self.__args_divider)
    1626                  template = options[0].strip()
    1627                  if template == '':
    1628                          template = '%s'
    1629
    1630                  return template % self.pat.export_as_gdt()
    1631
    1632          #--------------------------------------------------------
    1633 -        def _get_variant_patient_tags(self, data='%s//\\n'):
    1634                  if len(self.pat.tags) == 0:
    1635                          if self.debug:
    1636                                  return self._escape(_('no tags for this patient'))
    1637                          return ''
    1638
    1639                  tags = gmDemographicsWidgets.select_patient_tags(patient = self.pat)
    1640
    1641                  if tags is None:
    1642                          if self.debug:
    1643                                  return self._escape(_('no patient tags selected for inclusion') % data)
    1644                          return ''
    1645
    1646                  template, separator = data.split('//', 2)
    1647
    1648                  return separator.join([ template % t.fields_as_dict(escape_style = self.__esc_style) for t in tags ])
    1649  #       #--------------------------------------------------------
    1650  #       def _get_variant_patient_tags_table(self, data=u'?'):
    1651  #               pass
    1652          #--------------------------------------------------------
    1653          # praxis related placeholders
    1654          #--------------------------------------------------------
    1655 -        def _get_variant_praxis(self, data=None):
    1656                  options = data.split(self.__args_divider)
    1657
    1658                  if 'select' in options:
    1659                          options.remove('select')
    1660                          branch = 'select branch'
    1661                  else:
    1662                          branch = gmPraxis.cPraxisBranch(aPK_obj = gmPraxis.gmCurrentPraxisBranch()['pk_praxis_branch'])
    1663
    1664                  template = '%s'
    1665                  if len(options) > 0:
    1666                          template = options[0]
    1667                  if template.strip() == '':
    1668                          template = '%s'
    1669
    1670                  return template % branch.fields_as_dict(escape_style = self.__esc_style)
    1671
    1672          #--------------------------------------------------------
    1673 -        def _get_variant_praxis_vcf(self, data=None):
    1674
    1675                  cache_key = 'current_branch_vcf_path'
    1676                  try:
    1677                          vcf_name = self.__cache[cache_key]
    1678                          _log.debug('cache hit (%s): [%s]', cache_key, vcf_name)
    1679                  except KeyError:
    1680                          vcf_name = gmPraxis.gmCurrentPraxisBranch().vcf
    1681                          self.__cache[cache_key] = vcf_name
    1682
    1683                  template = '%s'
    1684                  if data.strip() != '':
    1685                          template = data
    1686
    1687                  return template % vcf_name
    1688
    1689          #--------------------------------------------------------
    1690 -        def _get_variant_praxis_mcf(self, data):
    1691                  template, format = self.__parse_ph_options (
    1692                          option_defs = {'tmpl': '%s', 'fmt': 'txt'},
    1693                          options_string = data
    1694                  )
    1695                  if format not in ['qr', 'mcf', 'txt']:
    1696                          if self.debug:
    1697                                  return self._escape(_('praxis_mcf: invalid format (qr/mcf/txt)'))
    1698                          return ''
    1699
    1700                  if format == 'txt':
    1701                          return template % self._escape(gmPraxis.gmCurrentPraxisBranch().MECARD)
    1702
    1703                  if format == 'mcf':
    1704                          return template % gmPraxis.gmCurrentPraxisBranch().export_as_mecard()
    1705
    1706                  if format == 'qr':
    1707                          qr_filename = gmTools.create_qrcode(text = gmPraxis.gmCurrentPraxisBranch().MECARD)
    1708                          if qr_filename is None:
    1709                                  return self._escape('praxis_mcf-cannot_create_QR_code')
    1710                          return template % qr_filename
    1711
    1712                  return None
    1713
    1714          #--------------------------------------------------------
    1715 -        def _get_variant_praxis_scan2pay(self, data):
    1716                  format = self.__parse_ph_options(option_defs = {'fmt': 'qr'}, options_string = data)
    1717                  if format not in ['qr', 'txt']:
    1718                          if self.debug:
    1719                                  return self._escape(_('praxis_scan2pay: invalid format (qr/txt)'))
    1720                          return u''
    1721
    1722                  data_str = gmPraxis.gmCurrentPraxisBranch().scan2pay_data
    1723                  if data_str is None:
    1724                          if self.debug:
    1725                                  return self._escape('praxis_scan2pay-cannot_create_data_file')
    1726                          return ''
    1727
    1728                  if format == 'txt':
    1729                          return self._escape(data_str)
    1730                          #return template % self._escape(gmPraxis.gmCurrentPraxisBranch().MECARD)
    1731
    1732                  if format == 'qr':
    1733                          qr_filename = gmTools.create_qrcode(text = data_str)
    1734                          if qr_filename is None:
    1735                                  if self.debug:
    1736                                          return self._escape('praxis_scan2pay-cannot_create_QR_code')
    1737                                  return ''
    1738                          #return template % qr_filename
    1739                          return qr_filename
    1740
    1741                  return None
    1742
    1743          #--------------------------------------------------------
    1744 -        def _get_variant_praxis_address(self, data=''):
    1745                  options = data.split(self.__args_divider)
    1746
    1747                  # formatting template
    1748                  template = _('%(street)s %(number)s, %(postcode)s %(urb)s, %(l10n_region)s, %(l10n_country)s')
    1749                  if len(options) > 0:
    1750                          if options[0].strip() != '':
    1751                                  template = options[0]
    1752
    1753                  adr = gmPraxis.gmCurrentPraxisBranch().address
    1754                  if adr is None:
    1755                          if self.debug:
    1756                                  return _('no address recorded')
    1757                          return ''
    1758                  try:
    1759                          return template % adr.fields_as_dict(escape_style = self.__esc_style)
    1760                  except Exception:
    1761                          _log.exception('error formatting address')
    1762                          _log.error('template: %s', template)
    1763
    1764                  return None
    1765
    1766          #--------------------------------------------------------
    1767 -        def _get_variant_praxis_comm(self, data=None):
    1768                  options = data.split(self.__args_divider)
    1769                  comm_type = options[0]
    1770                  template = '%(url)s'
    1771                  if len(options) > 1:
    1772                          template = options[1]
    1773
    1774                  comms = gmPraxis.gmCurrentPraxisBranch().get_comm_channels(comm_medium = comm_type)
    1775                  if len(comms) == 0:
    1776                          if self.debug:
    1777                                  return self._escape(_('no URL for comm channel [%s]') % data)
    1778                          return ''
    1779
    1780                  return template % comms[0].fields_as_dict(escape_style = self.__esc_style)
    1781
    1782          #--------------------------------------------------------
    1783 -        def _get_variant_praxis_id(self, data=None):
    1784                  options = data.split(self.__args_divider)
    1785                  id_type = options[0].strip()
    1786                  if id_type == '':
    1787                          return self._escape('praxis external ID: type is missing')
    1788
    1789                  if len(options) > 1:
    1790                          issuer = options[1].strip()
    1791                          if issuer == '':
    1792                                  issue = None
    1793                  else:
    1794                          issuer = None
    1795
    1796                  if len(options) > 2:
    1797                          template = options[2]
    1798                  else:
    1799                          template = '%(name)s: %(value)s (%(issuer)s)'
    1800
    1801                  ids = gmPraxis.gmCurrentPraxisBranch().get_external_ids(id_type = id_type, issuer = issuer)
    1802                  if len(ids) == 0:
    1803                          if self.debug:
    1804                                  return self._escape(_('no external ID [%s] by [%s]') % (id_type, issuer))
    1805                          return ''
    1806
    1807                  return template % self._escape_dict(the_dict = ids[0], none_string = '')
    1808
    1809          #--------------------------------------------------------
    1810          # provider related placeholders
    1811          #--------------------------------------------------------
    1812 -        def _get_variant_current_provider(self, data=None):
    1813                  prov = gmStaff.gmCurrentProvider()
    1814
    1815                  tmp = '%s%s. %s' % (
    1816                          gmTools.coalesce(prov['title'], '', '%s '),
    1817                          prov['firstnames'][:1],
    1818                          prov['lastnames']
    1819                  )
    1820                  return self._escape(tmp)
    1821
    1822          #--------------------------------------------------------
    1823 -        def _get_variant_current_provider_title(self, data=None):
    1824                  if data is None:
    1825                          template = u'%(title)s'
    1826                  elif data.strip() == u'':
    1827                          data = u'%(title)s'
    1828                  return self._get_variant_current_provider_name(data = data)
    1829
    1830          #--------------------------------------------------------
    1831 -        def _get_variant_current_provider_firstnames(self, data=None):
    1832                  if data is None:
    1833                          data = u'%(firstnames)s'
    1834                  elif data.strip() == u'':
    1835                          data = u'%(firstnames)s'
    1836                  return self._get_variant_current_provider_name(data = data)
    1837
    1838          #--------------------------------------------------------
    1839 -        def _get_variant_current_provider_lastnames(self, data=None):
    1840                  if data is None:
    1841                          data = u'%(lastnames)s'
    1842                  elif data.strip() == u'':
    1843                          data = u'%(lastnames)s'
    1844                  return self._get_variant_current_provider_name(data = data)
    1845
    1846          #--------------------------------------------------------
    1847 -        def _get_variant_current_provider_name(self, data=None):
    1848                  if data is None:
    1849                          return [_('template is missing')]
    1850                  if data.strip() == '':
    1851                          return [_('template is empty')]
    1852                  name = gmStaff.gmCurrentProvider().identity.get_active_name()
    1853                  parts = {
    1854                          'title': self._escape(gmTools.coalesce(name['title'], '')),
    1855                          'firstnames': self._escape(name['firstnames']),
    1856                          'lastnames': self._escape(name['lastnames']),
    1857                          'preferred': self._escape(gmTools.coalesce(name['preferred'], ''))
    1858                  }
    1859                  return data % parts
    1860
    1861          #--------------------------------------------------------
    1862 -        def _get_variant_current_provider_external_id(self, data=''):
    1863                  data_parts = data.split(self.__args_divider)
    1864                  if len(data_parts) < 2:
    1865                          return self._escape('current provider external ID: template is missing')
    1866
    1867                  id_type = data_parts[0].strip()
    1868                  if id_type == '':
    1869                          return self._escape('current provider external ID: type is missing')
    1870
    1871                  issuer = data_parts[1].strip()
    1872                  if issuer == '':
    1873                          return self._escape('current provider external ID: issuer is missing')
    1874
    1875                  prov = gmStaff.gmCurrentProvider()
    1876                  ids = prov.identity.get_external_ids(id_type = id_type, issuer = issuer)
    1877
    1878                  if len(ids) == 0:
    1879                          if self.debug:
    1880                                  return self._escape(_('no external ID [%s] by [%s]') % (id_type, issuer))
    1881                          return ''
    1882
    1883                  return self._escape(ids[0]['value'])
    1884
    1885          #--------------------------------------------------------
    1886 -        def _get_variant_primary_praxis_provider(self, data=None):
    1887                  prov = self.pat.primary_provider
    1888                  if prov is None:
    1889                          return self._get_variant_current_provider()
    1890
    1891                  title = gmTools.coalesce (
    1892                          prov['title'],
    1893                          gmPerson.map_gender2salutation(prov['gender'])
    1894                  )
    1895
    1896                  tmp = '%s %s. %s' % (
    1897                          title,
    1898                          prov['firstnames'][:1],
    1899                          prov['lastnames']
    1900                  )
    1901                  return self._escape(tmp)
    1902
    1903          #--------------------------------------------------------
    1904 -        def _get_variant_primary_praxis_provider_external_id(self, data=''):
    1905                  data_parts = data.split(self.__args_divider)
    1906                  if len(data_parts) < 2:
    1907                          return self._escape('primary in-praxis provider external ID: template is missing')
    1908
    1909                  id_type = data_parts[0].strip()
    1910                  if id_type == '':
    1911                          return self._escape('primary in-praxis provider external ID: type is missing')
    1912
    1913                  issuer = data_parts[1].strip()
    1914                  if issuer == '':
    1915                          return self._escape('primary in-praxis provider external ID: issuer is missing')
    1916
    1917                  prov = self.pat.primary_provider
    1918                  if prov is None:
    1919                          if self.debug:
    1920                                  return self._escape(_('no primary in-praxis provider'))
    1921                          return ''
    1922
    1923                  ids = prov.identity.get_external_ids(id_type = id_type, issuer = issuer)
    1924
    1925                  if len(ids) == 0:
    1926                          if self.debug:
    1927                                  return self._escape(_('no external ID [%s] by [%s]') % (id_type, issuer))
    1928                          return ''
    1929
    1930                  return self._escape(ids[0]['value'])
    1931
    1932          #--------------------------------------------------------
    1933 -        def _get_variant_external_id(self, data=''):
    1934                  data_parts = data.split(self.__args_divider)
    1935                  if len(data_parts) < 2:
    1936                          return self._escape('patient external ID: template is missing')
    1937
    1938                  id_type = data_parts[0].strip()
    1939                  if id_type == '':
    1940                          return self._escape('patient external ID: type is missing')
    1941
    1942                  issuer = data_parts[1].strip()
    1943                  if issuer == '':
    1944                          return self._escape('patient external ID: issuer is missing')
    1945
    1946                  ids = self.pat.get_external_ids(id_type = id_type, issuer = issuer)
    1947
    1948                  if len(ids) == 0:
    1949                          if self.debug:
    1950                                  return self._escape(_('no external ID [%s] by [%s]') % (id_type, issuer))
    1951                          return ''
    1952
    1953                  return self._escape(ids[0]['value'])
    1954
    1955          #--------------------------------------------------------
    1956 -        def _get_variant_allergy_state(self, data=None):
    1957                  allg_state = self.pat.emr.allergy_state
    1958
    1959                  if allg_state['last_confirmed'] is None:
    1960                          date_confirmed = ''
    1961                  else:
    1962                          date_confirmed = ' (%s)' % gmDateTime.pydt_strftime (
    1963                                  allg_state['last_confirmed'],
    1964                                  format = '%Y %B %d'
    1965                          )
    1966
    1967                  tmp = '%s%s' % (
    1968                          allg_state.state_string,
    1969                          date_confirmed
    1970                  )
    1971                  return self._escape(tmp)
    1972
    1973          #--------------------------------------------------------
    1974 -        def _get_variant_allergy_list(self, data=None):
    1975                  if data is None:
    1976                          return self._escape(_('template is missing'))
    1977
    1978                  template, separator = data.split('//', 2)
    1979
    1980                  return separator.join([ template % a.fields_as_dict(date_format = '%Y %b %d', escape_style = self.__esc_style) for a in self.pat.emr.get_allergies() ])
    1981
    1982          #--------------------------------------------------------
    1983 -        def _get_variant_allergies(self, data=None):
    1984
    1985                  if data is None:
    1986                          return self._escape(_('template is missing'))
    1987
    1988                  return '\n'.join([ data % a.fields_as_dict(date_format = '%Y %b %d', escape_style = self.__esc_style) for a in self.pat.emr.get_allergies() ])
    1989
    1990          #--------------------------------------------------------
    1991 -        def _get_variant_current_meds_AMTS_enhanced(self, data=None):
    1992                  return self._get_variant_current_meds_AMTS(data=data, strict=False)
    1993
    1994          #--------------------------------------------------------
    1995 -        def _get_variant_current_meds_AMTS(self, data=None, strict=True):
    1996
    1997                  # select intakes
    1998                  emr = self.pat.emr
    1999                  from Gnumed.wxpython import gmMedicationWidgets
    2000                  intakes2export = gmMedicationWidgets.manage_substance_intakes(emr = emr)
    2001                  if intakes2export is None:
    2002                          return ''
    2003                  if len(intakes2export) == 0:
    2004                          return ''
    2005
    2006                  # make them unique:
    2007                  unique_intakes = {}
    2008                  for intake in intakes2export:
    2009                          if intake['pk_drug_product'] is None:
    2010                                  unique_intakes[intake['pk_substance']] = intake
    2011                          else:
    2012                                  unique_intakes[intake['product']] = intake
    2013                  del intakes2export
    2014                  unique_intakes = unique_intakes.values()
    2015
    2016                  # create data files / datamatrix code files
    2017                  self.__create_amts_datamatrix_files(intakes = unique_intakes)
    2018
    2019                  # create AMTS-LaTeX per intake
    2020                  intake_as_latex_rows = []
    2021                  for intake in unique_intakes:
    2022                          intake_as_latex_rows.append(intake._get_as_amts_latex(strict = strict))
    2023                  del unique_intakes
    2024
    2025                  # append allergy information
    2026                  # - state
    2027                  intake_as_latex_rows.extend(emr.allergy_state._get_as_amts_latex(strict = strict))
    2028                  # - allergies
    2029                  for allg in emr.get_allergies():
    2030                          intake_as_latex_rows.append(allg._get_as_amts_latex(strict = strict))
    2031
    2032                  # insert \newpage after each group of 15 rows
    2033                  table_rows = intake_as_latex_rows[:15]
    2034                  if len(intake_as_latex_rows) > 15:
    2035                          table_rows.append('\\newpage')
    2036                          table_rows.extend(intake_as_latex_rows[15:30])
    2037                  if len(intake_as_latex_rows) > 30:
    2038                          table_rows.append('\\newpage')
    2039                          table_rows.extend(intake_as_latex_rows[30:45])
    2040
    2041                  if strict:
    2042                          return '\n'.join(table_rows)
    2043
    2044                  # allow two more pages in enhanced mode
    2045                  if len(intake_as_latex_rows) > 45:
    2046                          table_rows.append('\\newpage')
    2047                          table_rows.extend(intake_as_latex_rows[30:45])
    2048
    2049                  if len(intake_as_latex_rows) > 60:
    2050                          table_rows.append('\\newpage')
    2051                          table_rows.extend(intake_as_latex_rows[30:45])
    2052
    2053                  return '\n'.join(table_rows)
    2054
    2055          #--------------------------------------------------------
    2056 -        def __create_amts_datamatrix_files(self, intakes=None):
    2057
    2058                  # setup dummy files
    2059                  for idx in [1,2,3]:
    2060                          self.set_placeholder(key = 'amts_data_file_%s' % idx, value = './missing-file.txt', known_only = False)
    2061                          self.set_placeholder(key = 'amts_png_file_%s' % idx, value = './missing-file.png', known_only = False)
    2062                  self.set_placeholder(key = 'amts_png_file_current_page', value = './missing-file-current-page.png', known_only = False)
    2063                  self.set_placeholder(key = 'amts_png_file_utf8', value = './missing-file-utf8.png', known_only = False)
    2064                  self.set_placeholder(key = 'amts_data_file_utf8', value = './missing-file-utf8.txt', known_only = False)
    2065
    2066                  # find processor
    2067                  found, dmtx_creator = gmShellAPI.detect_external_binary(binary = 'gm-create_datamatrix')
    2068                  _log.debug(dmtx_creator)
    2069                  if not found:
    2070                          _log.error('gm-create_datamatrix(.bat/.exe) not found')
    2071                          return
    2072
    2073                  png_dir = gmTools.mk_sandbox_dir()
    2074                  _log.debug('sandboxing AMTS datamatrix PNGs in: %s', png_dir)
    2075
    2076                  from Gnumed.business import gmForms
    2077
    2078                  # generate GNUmed-enhanced non-conformant data file and datamatrix
    2079                  # for embedding (utf8, unabridged data fields)
    2080                  amts_data_template_def_file = gmMedication.generate_amts_data_template_definition_file(strict = False)
    2081                  _log.debug('amts data template definition file: %s', amts_data_template_def_file)
    2082                  form = gmForms.cTextForm(template_file = amts_data_template_def_file)
    2083                  # <S>ection with intakes</S>
    2084                  amts_sections = '<S>%s</S>' % ''.join ([
    2085                          i._get_as_amts_data(strict = False) for i in intakes
    2086                  ])
    2087                  # <S>ection with allergy data</S>
    2088                  emr = self.pat.emr
    2089                  amts_sections += emr.allergy_state._get_as_amts_data(strict = False) % ''.join ([
    2090                          a._get_as_amts_data(strict = False) for a in emr.get_allergies()
    2091                  ])
    2092                  self.set_placeholder(key = 'amts_intakes_as_data_enhanced', value = amts_sections, known_only = False)
    2093  #               self.set_placeholder(key = u'amts_check_symbol', value = gmMedication.calculate_amts_data_check_symbol(intakes = intakes), known_only = False)
    2094                  self.set_placeholder(key = 'amts_total_pages', value = '1', known_only = False)
    2095                  success = form.substitute_placeholders(data_source = self)
    2096                  self.unset_placeholder(key = 'amts_intakes_as_data_enhanced')
    2097  #               self.unset_placeholder(key = u'amts_check_symbol')
    2098                  self.unset_placeholder(key = 'amts_total_pages')
    2099                  if not success:
    2100                          _log.error('cannot substitute into amts data file form template')
    2101                          return
    2102                  data_file = form.re_editable_filenames[0]
    2103                  png_file = os.path.join(png_dir, 'gm4amts-datamatrix-utf8.png')
    2104                  cmd = '%s %s %s' % (dmtx_creator, data_file, png_file)
    2105                  success = gmShellAPI.run_command_in_shell(command = cmd, blocking = True)
    2106                  if not success:
    2107                          _log.error('error running [%s]' % cmd)
    2108                          return
    2109                  self.set_placeholder(key = 'amts_data_file_utf8', value = data_file, known_only = False)
    2110                  self.set_placeholder(key = 'amts_png_file_utf8', value = png_file, known_only = False)
    2111
    2112                  # generate conformant per-page files:
    2113                  total_pages = (len(intakes) / 15.0)
    2114                  if total_pages > int(total_pages):
    2115                          total_pages += 1
    2116                  total_pages = int(total_pages)
    2117                  _log.debug('total pages: %s', total_pages)
    2118
    2119                  png_file_base = os.path.join(png_dir, 'gm4amts-datamatrix-page-')
    2120                  for this_page in range(1,total_pages+1):
    2121                          intakes_this_page = intakes[(this_page-1)*15:this_page*15]
    2122                          amts_data_template_def_file = gmMedication.generate_amts_data_template_definition_file(strict = True)
    2123                          _log.debug('amts data template definition file: %s', amts_data_template_def_file)
    2124                          form = gmForms.cTextForm(template_file = amts_data_template_def_file)
    2125                          # <S>ection with intakes</S>
    2126                          amts_sections = '<S>%s</S>' % ''.join ([
    2127                                  i._get_as_amts_data(strict = False) for i in intakes_this_page
    2128                          ])
    2129                          if this_page == total_pages:
    2130                                  # <S>ection with allergy data</S>
    2131                                  emr = self.pat.emr
    2132                                  amts_sections += emr.allergy_state._get_as_amts_data(strict = False) % ''.join ([
    2133                                          a._get_as_amts_data(strict = False) for a in emr.get_allergies()
    2134                                  ])
    2135                          self.set_placeholder(key = 'amts_intakes_as_data', value = amts_sections, known_only = False)
    2136  #                       self.set_placeholder(key = u'amts_check_symbol', value = gmMedication.calculate_amts_data_check_symbol(intakes = intakes_this_page), known_only = False)
    2137                          if total_pages == 1:
    2138                                  pg_idx = ''
    2139                          else:
    2140                                  pg_idx = '%s' % this_page
    2141                          self.set_placeholder(key = 'amts_page_idx', value = pg_idx, known_only = False)
    2142                          self.set_placeholder(key = 'amts_total_pages', value = '%s' % total_pages, known_only = False)
    2143                          success = form.substitute_placeholders(data_source = self)
    2144                          self.unset_placeholder(key = 'amts_intakes_as_data')
    2145  #                       self.unset_placeholder(key = u'amts_check_symbol')
    2146                          self.unset_placeholder(key = 'amts_page_idx')
    2147                          self.unset_placeholder(key = 'amts_total_pages')
    2148                          if not success:
    2149                                  _log.error('cannot substitute into amts data file form template')
    2150                                  return
    2151
    2152                          data_file = form.re_editable_filenames[0]
    2153                          png_file = '%s%s.png' % (png_file_base, this_page)
    2154                          latin1_data_file = gmTools.recode_file (
    2155                                  source_file = data_file,
    2156                                  source_encoding = 'utf8',
    2157                                  target_encoding = 'latin1',
    2158                                  base_dir = os.path.split(data_file)[0]
    2159                          )
    2160                          cmd = '%s %s %s' % (dmtx_creator, latin1_data_file, png_file)
    2161                          success = gmShellAPI.run_command_in_shell(command = cmd, blocking = True)
    2162                          if not success:
    2163                                  _log.error('error running [%s]' % cmd)
    2164                                  return
    2165
    2166                          # cache file names for later use in \embedfile
    2167                          self.set_placeholder(key = 'amts_data_file_%s' % this_page, value = latin1_data_file, known_only = False)
    2168                          self.set_placeholder(key = 'amts_png_file_%s' % this_page, value = png_file, known_only = False)
    2169
    2170                  self.set_placeholder(key = 'amts_png_file_current_page', value = png_file_base + '\\thepage', known_only = False)
    2171
    2172          #--------------------------------------------------------
    2173 -        def _get_variant_current_meds_for_rx(self, data=None):
    2174                  if data is None:
    2175                          return self._escape(_('current_meds_for_rx: template is missing'))
    2176
    2177                  emr = self.pat.emr
    2178                  from Gnumed.wxpython import gmMedicationWidgets
    2179                  current_meds = gmMedicationWidgets.manage_substance_intakes(emr = emr)
    2180                  if current_meds is None:
    2181                          return ''
    2182
    2183                  intakes2show = {}
    2184                  for intake in current_meds:
    2185                          fields_dict = intake.fields_as_dict(date_format = '%Y %b %d', escape_style = self.__esc_style)
    2186                          fields_dict['medically_formatted_start'] = self._escape(intake.medically_formatted_start)
    2187                          if intake['pk_drug_product'] is None:
    2188                                  fields_dict['product'] = self._escape(_('generic %s') % fields_dict['substance'])
    2189                                  fields_dict['contains'] = self._escape('%s %s%s' % (fields_dict['substance'], fields_dict['amount'], fields_dict['unit']))
    2190                                  intakes2show[fields_dict['product']] = fields_dict
    2191                          else:
    2192                                  comps = [ c.split('::') for c in intake.containing_drug['components'] ]
    2193                                  fields_dict['contains'] = self._escape('; '.join([ '%s %s%s' % (c[0], c[1], c[2]) for c in comps ]))
    2194                                  intakes2show[intake['product']] = fields_dict           # this will make multi-component drugs unique
    2195
    2196                  intakes2dispense = {}
    2197                  for product, intake in intakes2show.items():
    2198                          msg = _('Dispense how much/many of "%(product)s (%(contains)s)" ?') % intake
    2199                          amount2dispense = wx.GetTextFromUser(msg, _('Amount to dispense ?'))
    2200                          if amount2dispense == '':
    2201                                  continue
    2202                          intake['amount2dispense'] = amount2dispense
    2203                          intakes2dispense[product] = intake
    2204
    2205                  return '\n'.join([ data % intake for intake in intakes2dispense.values() ])
    2206
    2207          #--------------------------------------------------------
    2208 -        def _get_variant_substance_abuse(self, data=None):
    2209                  if data is None:
    2210                          return self._escape(_('template is missing'))
    2211                  template = data
    2212                  from Gnumed.wxpython import gmHabitWidgets
    2213                  abuses = gmHabitWidgets.manage_substance_abuse(patient = self.pat)
    2214                  if abuses is None:
    2215                          return ''
    2216                  lines = []
    2217                  for a in abuses:
    2218                          fields = a.fields_as_dict(date_format = '%Y %b %d', escape_style = self.__esc_style)
    2219                          fields['harmful_use_type'] = a.harmful_use_type_string
    2220                          lines.append(template % fields)
    2221                  return '\n'.join(lines)
    2222
    2223          #--------------------------------------------------------
    2224 -        def _get_variant_current_meds(self, data=None):
    2225
    2226                  if data is None:
    2227                          return self._escape(_('template is missing'))
    2228
    2229                  parts = data.split(self.__args_divider)
    2230                  template = parts[0]
    2231                  ask_user = False
    2232                  if len(parts) > 1:
    2233                          ask_user = (parts[1] == 'select')
    2234
    2235                  emr = self.pat.emr
    2236                  if ask_user:
    2237                          from Gnumed.wxpython import gmMedicationWidgets
    2238                          current_meds = gmMedicationWidgets.manage_substance_intakes(emr = emr)
    2239                          if current_meds is None:
    2240                                  return ''
    2241                  else:
    2242                          current_meds = emr.get_current_medications (
    2243                                  include_inactive = False,
    2244                                  include_unapproved = True,
    2245                                  order_by = 'product, substance'
    2246                          )
    2247                          if len(current_meds) == 0:
    2248                                  return ''
    2249
    2250                  lines = []
    2251                  for m in current_meds:
    2252                          data = m.fields_as_dict(date_format = '%Y %b %d', escape_style = self.__esc_style)
    2253                          data['medically_formatted_start'] = self._escape(m.medically_formatted_start)
    2254                          lines.append(template % data)
    2255
    2256                  return '\n'.join(lines)
    2257          #--------------------------------------------------------
    2258 -        def _get_variant_current_meds_table(self, data=None):
    2259                  return gmMedication.format_substance_intake (
    2260                          emr = self.pat.emr,
    2261                          output_format = self.__esc_style,
    2262                          table_type = 'by-product'
    2263                  )
    2264          #--------------------------------------------------------
    2265 -        def _get_variant_current_meds_notes(self, data=None):
    2266                  return gmMedication.format_substance_intake_notes (
    2267                          emr = self.pat.emr,
    2268                          output_format = self.__esc_style,
    2269                          table_type = 'by-product'
    2270                  )
    2271          #--------------------------------------------------------
    2272 -        def _get_variant_lab_table(self, data=None):
    2273                  return gmPathLab.format_test_results (
    2274                          results = self.pat.emr.get_test_results_by_date(),
    2275                          output_format = self.__esc_style
    2276                  )
    2277
    2278          #--------------------------------------------------------
    2279 -        def _get_variant_test_results(self, data=None):
    2280
    2281                  template = ''
    2282                  date_format = '%Y %b %d %H:%M'
    2283                  separator = '\n'
    2284                  options = data.split(self.__args_divider)
    2285                  try:
    2286                          template = options[0].strip()
    2287                          date_format = options[1]
    2288                          separator = options[2]
    2289                  except IndexError:
    2290                          pass
    2291
    2292                  if date_format.strip() == '':
    2293                          date_format = '%Y %b %d %H:%M'
    2294                  if separator.strip() == '':
    2295                          separator = '\n'
    2296
    2297                  from Gnumed.wxpython.gmMeasurementWidgets import manage_measurements
    2298                  results = manage_measurements(single_selection = False, emr = self.pat.emr)
    2299                  if results is None:
    2300                          if self.debug:
    2301                                  return self._escape(_('no results for this patient (available or selected)'))
    2302                          return ''
    2303
    2304                  if template == '':
    2305                          return (separator + separator).join([ self._escape(r.format(date_format = date_format)) for r in results ])
    2306
    2307                  return separator.join([ template % r.fields_as_dict(date_format = date_format, escape_style = self.__esc_style) for r in results ])
    2308
    2309          #--------------------------------------------------------
    2310 -        def _get_variant_most_recent_test_results(self, data=None):
    2311                  most_recent = gmPathLab.get_most_recent_result_for_test_types (
    2312                          pk_test_types = None,                   # we want most recent results for *all* tests
    2313                          pk_patient = self.pat.ID,
    2314                          consider_meta_type = True,
    2315                          order_by = 'unified_name'
    2316                  )
    2317                  if len(most_recent) == 0:
    2318                          if self.debug:
    2319                                  return self._escape(_('no results for this patient available'))
    2320                          return ''
    2321
    2322                  from Gnumed.wxpython.gmMeasurementWidgets import manage_measurements
    2323                  results2show = manage_measurements (
    2324                          single_selection = False,
    2325                          measurements2manage = most_recent,
    2326                          message = _('Most recent results: select the ones to include')
    2327                  )
    2328                  if results2show is None:
    2329                          if self.debug:
    2330                                  return self._escape(_('no results for this patient selected'))
    2331                          return ''
    2332
    2333                  template, date_format, separator = self.__parse_ph_options (
    2334                          option_defs = {'tmpl': '', 'dfmt': '%Y %b %d', 'sep': '\n'},
    2335                          options_string = data
    2336                  )
    2337                  if template == '':
    2338                          return (separator + separator).join([ self._escape(r.format(date_format = date_format)) for r in results2show ])
    2339
    2340                  return separator.join([ template % r.fields_as_dict(date_format = date_format, escape_style = self.__esc_style) for r in results2show ])
    2341
    2342          #--------------------------------------------------------
    2343 -        def _get_variant_latest_vaccs_table(self, data=None):
    2344                  return gmVaccination.format_latest_vaccinations (
    2345                          output_format = self.__esc_style,
    2346                          emr = self.pat.emr
    2347                  )
    2348          #--------------------------------------------------------
    2349 -        def _get_variant_vaccination_history(self, data=None):
    2350                  options = data.split(self.__args_divider)
    2351                  template = options[0]
    2352                  if len(options) > 1:
    2353                          date_format = options[1]
    2354                  else:
    2355                          date_format = '%Y %b %d'
    2356                  vaccinations_as_dict = []
    2357                  for v in self.pat.emr.get_vaccinations(order_by = 'date_given DESC, vaccine'):
    2358                          v_as_dict = v.fields_as_dict(date_format = date_format, escape_style = self.__esc_style)
    2359                          v_as_dict['l10n_indications'] = [ ind['l10n_indication'] for ind in v['indications'] ]
    2360                          vaccinations_as_dict.append(v_as_dict)
    2361
    2362                  return u'\n'.join([ template % v for v in vaccinations_as_dict ])
    2363
    2364          #--------------------------------------------------------
    2365 -        def _get_variant_PHX(self, data=None):
    2366
    2367                  if data is None:
    2368                          if self.debug:
    2369                                  _log.error('PHX: missing placeholder arguments')
    2370                                  return self._escape(_('PHX: Invalid placeholder options.'))
    2371                          return ''
    2372
    2373                  _log.debug('arguments: %s', data)
    2374
    2375                  data_parts = data.split(self.__args_divider)
    2376                  template = '%s'
    2377                  separator = '\n'
    2378                  date_format = '%Y %b %d'
    2379                  try:
    2380                          template = data_parts[0]
    2381                          separator = data_parts[1]
    2382                          date_format = data_parts[2]
    2383                  except IndexError:
    2384                          pass
    2385
    2386                  phxs = gmEMRStructWidgets.select_health_issues(emr = self.pat.emr)
    2387                  if phxs is None:
    2388                          if self.debug:
    2389                                  return self._escape(_('no PHX for this patient (available or selected)'))
    2390                          return ''
    2391
    2392                  return separator.join ([
    2393                          template % phx.fields_as_dict (
    2394                                  date_format = date_format,
    2395                                  escape_style = self.__esc_style,
    2396                                  bool_strings = (self._escape(_('yes')), self._escape(_('no')))
    2397                          ) for phx in phxs
    2398                  ])
    2399
    2400          #--------------------------------------------------------
    2401 -        def _get_variant_problems(self, data=None):
    2402
    2403                  if data is None:
    2404                          return self._escape(_('template is missing'))
    2405                  probs = self.pat.emr.get_problems()
    2406                  return '\n'.join([ data % p.fields_as_dict(date_format = '%Y %b %d', escape_style = self.__esc_style) for p in probs ])
    2407
    2408          #--------------------------------------------------------
    2409 -        def _get_variant_diagnoses(self, data=None):
    2410
    2411                  if data is None:
    2412                          return self._escape(_('template is missing'))
    2413                  template = data
    2414                  dxs = self.pat.emr.candidate_diagnoses
    2415                  if len(dxs) == 0:
    2416                          _log.debug('no diagnoses available')
    2417                          return ''
    2418                  selected = gmListWidgets.get_choices_from_list (
    2419                          msg = _('Select the relevant diagnoses:'),
    2420                          caption = _('Diagnosis selection'),
    2421                          columns = [ _('Diagnosis'), _('Marked confidential'), _('Certainty'), _('Source') ],
    2422                          choices = [[
    2423                                  dx['diagnosis'],
    2424                                  gmTools.bool2subst(dx['explicitely_confidential'], _('yes'), _('no'), _('unknown')),
    2425                                  gmTools.coalesce(dx['diagnostic_certainty_classification'], ''),
    2426                                  dx['source']
    2427                                  ] for dx in dxs
    2428                          ],
    2429                          data = dxs,
    2430                          single_selection = False,
    2431                          can_return_empty = True
    2432                  )
    2433                  if selected is None:
    2434                          _log.debug('user did not select any diagnoses')
    2435                          return ''
    2436                  if len(selected) == 0:
    2437                          _log.debug('user did not select any diagnoses')
    2438                          return ''
    2439                          #return template % {'diagnosis': u'', 'diagnostic_certainty_classification': u''}
    2440                  return '\n'.join(template % self._escape_dict(dx, none_string = '?', bool_strings = [_('yes'), _('no')]) for dx in selected)
    2441
    2442          #--------------------------------------------------------
    2443 -        def _get_variant_today(self, data='%Y %b %d'):
    2444                  return self._escape(gmDateTime.pydt_now_here().strftime(data))
    2445
    2446          #--------------------------------------------------------
    2447 -        def _get_variant_tex_escape(self, data=None):
    2448                  return gmTools.tex_escape_string(text = data)
    2449
    2450          #--------------------------------------------------------
    2451 -        def _get_variant_url_escape(self, data=None):
    2452                  return self._escape(urllib.parse.quote(data.encode('utf8')))
    2453
    2454          #--------------------------------------------------------
    2455 -        def _get_variant_text_snippet(self, data=None):
    2456                  data_parts = data.split(self.__args_divider)
    2457                  keyword = data_parts[0]
    2458                  template = '%s'
    2459                  if len(data_parts) > 1:
    2460                          template = data_parts[1]
    2461
    2462                  expansion = gmKeywordExpansionWidgets.expand_keyword(keyword = keyword, show_list_if_needed = True)
    2463
    2464                  if expansion is None:
    2465                          if self.debug:
    2466                                  return self._escape(_('no textual expansion found for keyword <%s>') % keyword)
    2467                          return ''
    2468
    2469                  #return template % self._escape(expansion)
    2470                  return template % expansion
    2471
    2472          #--------------------------------------------------------
    2473 -        def _get_variant_data_snippet(self, data=None):
    2474                  parts = data.split(self.__args_divider)
    2475                  keyword = parts[0]
    2476                  template = '%s'
    2477                  target_mime = None
    2478                  target_ext = None
    2479                  if len(parts) > 1:
    2480                          template = parts[1]
    2481                  if len(parts) > 2:
    2482                          if parts[2].strip() != '':
    2483                                  target_mime = parts[2].strip()
    2484                  if len(parts) > 3:
    2485                          if parts[3].strip() != '':
    2486                                  target_ext = parts[3].strip()
    2487
    2488                  expansion = gmKeywordExpansion.get_expansion (
    2489                          keyword = keyword,
    2490                          textual_only = False,
    2491                          binary_only = True
    2492                  )
    2493                  if expansion is None:
    2494                          if self.debug:
    2495                                  return self._escape(_('no binary expansion found for keyword <%s>') % keyword)
    2496                          return ''
    2497
    2498                  saved_fname = expansion.save_to_file()
    2499                  if saved_fname is None:
    2500                          if self.debug:
    2501                                  return self._escape(_('cannot export data of binary expansion keyword <%s>') % keyword)
    2502                          return ''
    2503
    2504                  if expansion['is_encrypted']:
    2505                          pwd = wx.GetPasswordFromUser (
    2506                                  message = _('Enter your GnuPG passphrase for decryption of [%s]') % expansion['keyword'],
    2507                                  caption = _('GnuPG passphrase prompt'),
    2508                                  default_value = ''
    2509                          )
    2510                          saved_fname = gmCrypto.gpg_decrypt_file(filename = saved_fname, passphrase = pwd)
    2511                          if saved_fname is None:
    2512                                  if self.debug:
    2513                                          return self._escape(_('cannot decrypt data of binary expansion keyword <%s>') % keyword)
    2514                                  return ''
    2515
    2516                  if target_mime is None:
    2517                          return template % saved_fname
    2518
    2519                  converted_fname = gmMimeLib.convert_file(filename = saved_fname, target_mime = target_mime, target_extension = target_ext)
    2520                  if converted_fname is None:
    2521                          if self.debug:
    2522                                  return self._escape(_('cannot convert data of binary expansion keyword <%s>') % keyword)
    2523                          # hoping that the target can cope:
    2524                          return template % saved_fname
    2525
    2526                  return template % converted_fname
    2527
    2528          #--------------------------------------------------------
    2529 -        def _get_variant_qrcode(self, data=None):
    2530                  options = data.split(self.__args_divider)
    2531                  if len(options) == 0:
    2532                          return None
    2533                  text4qr = options[0]
    2534                  if len(options) > 1:
    2535                          template = options[1]
    2536                  else:
    2537                          template = u'%s'
    2538                  qr_filename = gmTools.create_qrcode(text = text4qr)
    2539                  if qr_filename is None:
    2540                          return self._escape('cannot_create_QR_code')
    2541
    2542                  return template % qr_filename
    2543
    2544          #--------------------------------------------------------
    2545 -        def _get_variant_range_of(self, data=None):
    2546                  if data is None:
    2547                          return None
    2548                  # wrapper code already takes care of actually
    2549                  # selecting the range so all we need to do here
    2550                  # is to return the data itself
    2551                  return data
    2552
    2553          #--------------------------------------------------------
    2554 -        def _get_variant_if_not_empty(self, data=None):
    2555                  if data is None:
    2556                          return None
    2557
    2558                  parts = data.split(self.__args_divider)
    2559                  if len(parts) < 3:
    2560                          return 'IF_NOT_EMPTY lacks <instead> definition'
    2561                  txt = parts[0]
    2562                  template = parts[1]
    2563                  instead = parts[2]
    2564
    2565                  if txt.strip() == '':
    2566                          return instead
    2567                  if '%s' in template:
    2568                          return template % txt
    2569                  return template
    2570
    2571          #--------------------------------------------------------
    2572 -        def _get_variant_if_debugging(self, data=None):
    2573
    2574                  if data is None:
    2575                          return None
    2576                  parts = data.split(self.__args_divider)
    2577                  if len(parts) < 2:
    2578                          return self._escape(u'IF_DEBUGGING lacks proper definition')
    2579                  debug_str = parts[0]
    2580                  non_debug_str = parts[1]
    2581                  if self.debug:
    2582                          return debug_str
    2583                  return non_debug_str
    2584
    2585          #--------------------------------------------------------
    2586 -        def _get_variant_free_text(self, data=None):
    2587
    2588                  if data is None:
    2589                          parts = []
    2590                          msg = _('generic text')
    2591                          cache_key = 'free_text::%s' % datetime.datetime.now()
    2592                  else:
    2593                          parts = data.split(self.__args_divider)
    2594                          msg = parts[0]
    2595                          cache_key = 'free_text::%s' % msg
    2596
    2597                  try:
    2598                          return self.__cache[cache_key]
    2599                  except KeyError:
    2600                          pass
    2601
    2602                  if len(parts) > 1:
    2603                          preset = parts[1]
    2604                  else:
    2605                          preset = ''
    2606
    2607                  dlg = gmGuiHelpers.cMultilineTextEntryDlg (
    2608                          None,
    2609                          -1,
    2610                          title = _('Replacing <free_text> placeholder'),
    2611                          msg = _('Below you can enter free text.\n\n [%s]') % msg,
    2612                          text = preset
    2613                  )
    2614                  dlg.enable_user_formatting = True
    2615                  decision = dlg.ShowModal()
    2616                  text = dlg.value.strip()
    2617                  is_user_formatted = dlg.is_user_formatted
    2618                  dlg.DestroyLater()
    2619
    2620                  if decision != wx.ID_SAVE:
    2621                          if self.debug:
    2622                                  return self._escape(_('Text input cancelled by user.'))
    2623                          return self._escape('')
    2624
    2625                  # user knows "best"
    2626                  if is_user_formatted:
    2627                          self.__cache[cache_key] = text
    2628                          return text
    2629
    2630                  text = self._escape(text)
    2631                  self.__cache[cache_key] = text
    2632                  return text
    2633
    2634          #--------------------------------------------------------
    2635 -        def _get_variant_bill(self, data=None):
    2636                  try:
    2637                          bill = self.__cache['bill']
    2638                  except KeyError:
    2639                          from Gnumed.wxpython import gmBillingWidgets
    2640                          bill = gmBillingWidgets.manage_bills(patient = self.pat)
    2641                          if bill is None:
    2642                                  if self.debug:
    2643                                          return self._escape(_('no bill selected'))
    2644                                  return ''
    2645                          self.__cache['bill'] = bill
    2646
    2647                  parts = data.split(self.__args_divider)
    2648                  template = parts[0]
    2649                  if len(parts) > 1:
    2650                          date_format = parts[1]
    2651                  else:
    2652                          date_format = '%Y %B %d'
    2653
    2654                  return template % bill.fields_as_dict(date_format = date_format, escape_style = self.__esc_style)
    2655
    2656          #--------------------------------------------------------
    2657 -        def _get_variant_bill_scan2pay(self, data=None):
    2658                  try:
    2659                          bill = self.__cache['bill']
    2660                  except KeyError:
    2661                          from Gnumed.wxpython import gmBillingWidgets
    2662                          bill = gmBillingWidgets.manage_bills(patient = self.pat)
    2663                          if bill is None:
    2664                                  if self.debug:
    2665                                          return self._escape(_('no bill selected'))
    2666                                  return ''
    2667                          self.__cache['bill'] = bill
    2668
    2669                  format = self.__parse_ph_options(option_defs = {'fmt': 'qr'}, options_string = data)
    2670                  if format not in ['qr', 'txt']:
    2671                          if self.debug:
    2672                                  return self._escape(_('praxis_scan2pay: invalid format (qr/txt)'))
    2673                          return ''
    2674
    2675                  from Gnumed.business import gmBilling
    2676                  data_str = gmBilling.get_scan2pay_data (
    2677                          gmPraxis.gmCurrentPraxisBranch(),
    2678                          bill,
    2679                          provider = gmStaff.gmCurrentProvider()
    2680                  )
    2681                  if data_str is None:
    2682                          if self.debug:
    2683                                  return self._escape('bill_scan2pay-cannot_create_data_file')
    2684                          return ''
    2685
    2686                  if format == 'txt':
    2687                          return self._escape(data_str)
    2688
    2689                  if format == 'qr':
    2690                          qr_filename = gmTools.create_qrcode(text = data_str)
    2691                          if qr_filename is not None:
    2692                                  return qr_filename
    2693                          if self.debug:
    2694                                  return self._escape('bill_scan2pay-cannot_create_QR_code')
    2695                          return ''
    2696
    2697                  return None
    2698
    2699          #--------------------------------------------------------
    2700 -        def _get_variant_bill_item(self, data=None):
    2701                  try:
    2702                          bill = self.__cache['bill']
    2703                  except KeyError:
    2704                          from Gnumed.wxpython import gmBillingWidgets
    2705                          bill = gmBillingWidgets.manage_bills(patient = self.pat)
    2706                          if bill is None:
    2707                                  if self.debug:
    2708                                          return self._escape(_('no bill selected'))
    2709                                  return ''
    2710                          self.__cache['bill'] = bill
    2711
    2712                  parts = data.split(self.__args_divider)
    2713                  template = parts[0]
    2714                  if len(parts) > 1:
    2715                          date_format = parts[1]
    2716                  else:
    2717                          date_format = '%Y %B %d'
    2718
    2719                  return '\n'.join([ template % i.fields_as_dict(date_format = date_format, escape_style = self.__esc_style) for i in bill.bill_items ])
    2720
    2721          #--------------------------------------------------------
    2722 -        def __get_variant_bill_adr_part(self, data=None, part=None):
    2723                  try:
    2724                          bill = self.__cache['bill']
    2725                  except KeyError:
    2726                          from Gnumed.wxpython import gmBillingWidgets
    2727                          bill = gmBillingWidgets.manage_bills(patient = self.pat)
    2728                          if bill is None:
    2729                                  if self.debug:
    2730                                          return self._escape(_('no bill selected'))
    2731                                  return ''
    2732                          self.__cache['bill'] = bill
    2733                          self.__cache['bill-adr'] = bill.address
    2734
    2735                  try:
    2736                          bill_adr = self.__cache['bill-adr']
    2737                  except KeyError:
    2738                          bill_adr = bill.address
    2739                          self.__cache['bill-adr'] = bill_adr
    2740
    2741                  if bill_adr is None:
    2742                          if self.debug:
    2743                                  return self._escape(_('[%s] bill has no address') % part)
    2744                          return ''
    2745
    2746                  if bill_adr[part] is None:
    2747                          return self._escape('')
    2748
    2749                  if data is None:
    2750                          return self._escape(bill_adr[part])
    2751
    2752                  if data == '':
    2753                          return self._escape(bill_adr[part])
    2754
    2755                  return data % self._escape(bill_adr[part])
    2756
    2757          #--------------------------------------------------------
    2758 -        def _get_variant_bill_adr_street(self, data='?'):
    2759                  return self.__get_variant_bill_adr_part(data = data, part = 'street')
    2760
    2761          #--------------------------------------------------------
    2762 -        def _get_variant_bill_adr_number(self, data='?'):
    2763                  return self.__get_variant_bill_adr_part(data = data, part = 'number')
    2764
    2765          #--------------------------------------------------------
    2766 -        def _get_variant_bill_adr_subunit(self, data='?'):
    2767                  return self.__get_variant_bill_adr_part(data = data, part = 'subunit')
    2768          #--------------------------------------------------------
    2769 -        def _get_variant_bill_adr_location(self, data='?'):
    2770                  return self.__get_variant_bill_adr_part(data = data, part = 'urb')
    2771
    2772          #--------------------------------------------------------
    2773 -        def _get_variant_bill_adr_suburb(self, data='?'):
    2774                  return self.__get_variant_bill_adr_part(data = data, part = 'suburb')
    2775
    2776          #--------------------------------------------------------
    2777 -        def _get_variant_bill_adr_postcode(self, data='?'):
    2778                  return self.__get_variant_bill_adr_part(data = data, part = 'postcode')
    2779
    2780          #--------------------------------------------------------
    2781 -        def _get_variant_bill_adr_region(self, data='?'):
    2782                  return self.__get_variant_bill_adr_part(data = data, part = 'l10n_region')
    2783
    2784          #--------------------------------------------------------
    2785 -        def _get_variant_bill_adr_country(self, data='?'):
    2786                  return self.__get_variant_bill_adr_part(data = data, part = 'l10n_country')
    2787
    2788          #--------------------------------------------------------
    2789          # internal helpers
    2790          #--------------------------------------------------------
    2791 -        def __parse_ph_options(self, option_defs=None, options_string=None):
    2792                  """Returns a tuple of values in the order of option_defs.keys()."""
    2793                  assert (option_defs is not None), '<option_defs> must not be None'
    2794                  assert (options_string is not None), '<options_string> must not be None'
    2795
    2796                  _log.debug('parsing ::%s:: for %s', options_string, option_defs)
    2797                  options2return = option_defs.copy()
    2798                  options = options_string.split(self.__args_divider)
    2799                  for opt in options:
    2800                          opt_name, opt_val = opt.split('=', 1)
    2801                          if opt_name.strip() not in option_defs.keys():
    2802                                  continue
    2803                          options2return[opt_name] = opt_val
    2804
    2805                  _log.debug('found: %s', options2return)
    2806                  return tuple([ options2return[o_name] for o_name in options2return.keys() ])
    2807
    2808          #--------------------------------------------------------
    2809 -        def _escape(self, text=None):
    2810                  if self.__esc_func is None:
    2811                          return text
    2812                  assert (text is not None), 'text=None passed to _escape()'
    2813                  return self.__esc_func(text)
    2814
    2815          #--------------------------------------------------------
    2816 -        def _escape_dict(self, the_dict=None, date_format='%Y %b %d  %H:%M', none_string='', bool_strings=None):
    2817                  if bool_strings is None:
    2818                          bools = {True: _('true'), False: _('false')}
    2819                  else:
    2820                          bools = {True: bool_strings[0], False: bool_strings[1]}
    2821                  data = {}
    2822                  for field in the_dict.keys():
    2823                          # FIXME: harden against BYTEA fields
    2824                          #if type(self._payload[self._idx[field]]) == ...
    2825                          #       data[field] = _('<%s bytes of binary data>') % len(self._payload[self._idx[field]])
    2826                          #       continue
    2827                          val = the_dict[field]
    2828                          if val is None:
    2829                                  data[field] = none_string
    2830                                  continue
    2831                          if isinstance(val, bool):
    2832                                  data[field] = bools[val]
    2833                                  continue
    2834                          if isinstance(val, datetime.datetime):
    2835                                  data[field] = gmDateTime.pydt_strftime(val, format = date_format)
    2836                                  if self.__esc_style in ['latex', 'tex']:
    2837                                          data[field] = gmTools.tex_escape_string(data[field])
    2838                                  elif self.__esc_style in ['xetex', 'xelatex']:
    2839                                          data[field] = gmTools.xetex_escape_string(data[field])
    2840                                  continue
    2841                          try:
    2842                                  data[field] = str(val, encoding = 'utf8', errors = 'replace')
    2843                          except TypeError:
    2844                                  try:
    2845                                          data[field] = str(val)
    2846                                  except (UnicodeDecodeError, TypeError):
    2847                                          val = '%s' % str(val)
    2848                                          data[field] = val.decode('utf8', 'replace')
    2849                          if self.__esc_style in ['latex', 'tex']:
    2850                                  data[field] = gmTools.tex_escape_string(data[field])
    2851                          elif self.__esc_style in ['xetex', 'xelatex']:
    2852                                  data[field] = gmTools.xetex_escape_string(data[field])
    2853                  return data
    2854
    2855  #---------------------------------------------------------------------
    2856 -def test_placeholders():
    2857
    2858          _log.debug('testing for placeholders with pattern: %s', first_pass_placeholder_regex)
    2859
    2860          data_source = gmPlaceholderHandler()
    2861          original_line = ''
    2862
    2863          while True:
    2864                  # get input from user
    2865                  line = wx.GetTextFromUser (
    2866                          _('Enter some text containing a placeholder:'),
    2867                          _('Testing placeholders'),
    2868                          centre = True,
    2869                          default_value = original_line
    2870                  )
    2871                  if line.strip() == '':
    2872                          break
    2873                  original_line = line
    2874                  # replace
    2875                  placeholders_in_line = regex.findall(first_pass_placeholder_regex, line, regex.IGNORECASE)
    2876                  if len(placeholders_in_line) == 0:
    2877                          continue
    2878                  for placeholder in placeholders_in_line:
    2879                          try:
    2880                                  val = data_source[placeholder]
    2881                          except Exception:
    2882                                  val = _('error with placeholder [%s]') % placeholder
    2883                          if val is None:
    2884                                  val = _('error with placeholder [%s]') % placeholder
    2885                          line = line.replace(placeholder, val)
    2886                  # show
    2887                  msg = _(
    2888                          'Input: %s\n'
    2889                          '\n'
    2890                          'Output:\n'
    2891                          '%s'
    2892                  ) % (
    2893                          original_line,
    2894                          line
    2895                  )
    2896                  gmGuiHelpers.gm_show_info (
    2897                          title = _('Testing placeholders'),
    2898                          info = msg
    2899                  )
    2900
    2901  #=====================================================================
    2902 -class cMacroPrimitives:
    2903          """Functions a macro can legally use.
    2904
    2905          An instance of this class is passed to the GNUmed scripting
    2906          listener. Hence, all actions a macro can legally take must
    2907          be defined in this class. Thus we achieve some screening for
    2908          security and also thread safety handling.
    2909          """
    2910          #-----------------------------------------------------------------
    2911 -        def __init__(self, personality = None):
    2912                  if personality is None:
    2913                          raise gmExceptions.ConstructorError('must specify personality')
    2914                  self.__personality = personality
    2915                  self.__attached = 0
    2916                  self._get_source_personality = None
    2917                  self.__user_done = False
    2918                  self.__user_answer = 'no answer yet'
    2919                  self.__pat = gmPerson.gmCurrentPatient()
    2920
    2921                  self.__auth_cookie = str(random.random())
    2922                  self.__pat_lock_cookie = str(random.random())
    2923                  self.__lock_after_load_cookie = str(random.random())
    2924
    2925                  _log.info('slave mode personality is [%s]', personality)
    2926          #-----------------------------------------------------------------
    2927          # public API
    2928          #-----------------------------------------------------------------
    2929 -        def attach(self, personality = None):
    2930                  if self.__attached:
    2931                          _log.error('attach with [%s] rejected, already serving a client', personality)
    2932                          return (0, _('attach rejected, already serving a client'))
    2933                  if personality != self.__personality:
    2934                          _log.error('rejecting attach to personality [%s], only servicing [%s]' % (personality, self.__personality))
    2935                          return (0, _('attach to personality [%s] rejected') % personality)
    2936                  self.__attached = 1
    2937                  self.__auth_cookie = str(random.random())
    2938                  return (1, self.__auth_cookie)
    2939          #-----------------------------------------------------------------
    2940 -        def detach(self, auth_cookie=None):
    2941                  if not self.__attached:
    2942                          return 1
    2943                  if auth_cookie != self.__auth_cookie:
    2944                          _log.error('rejecting detach() with cookie [%s]' % auth_cookie)
    2945                          return 0
    2946                  self.__attached = 0
    2947                  return 1
    2948          #-----------------------------------------------------------------
    2949 -        def force_detach(self):
    2950                  if not self.__attached:
    2951                          return 1
    2952                  self.__user_done = False
    2953                  # FIXME: use self.__sync_cookie for syncing with user interaction
    2954                  wx.CallAfter(self._force_detach)
    2955                  return 1
    2956          #-----------------------------------------------------------------
    2957 -        def version(self):
    2958                  ver = _cfg.get(option = 'client_version')
    2959                  return "GNUmed %s, %s $Revision: 1.51 $" % (ver, self.__class__.__name__)
    2960          #-----------------------------------------------------------------
    2961 -        def shutdown_gnumed(self, auth_cookie=None, forced=False):
    2962                  """Shuts down this client instance."""
    2963                  if not self.__attached:
    2964                          return 0
    2965                  if auth_cookie != self.__auth_cookie:
    2966                          _log.error('non-authenticated shutdown_gnumed()')
    2967                          return 0
    2968                  wx.CallAfter(self._shutdown_gnumed, forced)
    2969                  return 1
    2970          #-----------------------------------------------------------------
    2971 -        def raise_gnumed(self, auth_cookie = None):
    2972                  """Raise ourselves to the top of the desktop."""
    2973                  if not self.__attached:
    2974                          return 0
    2975                  if auth_cookie != self.__auth_cookie:
    2976                          _log.error('non-authenticated raise_gnumed()')
    2977                          return 0
    2978                  return "cMacroPrimitives.raise_gnumed() not implemented"
    2979          #-----------------------------------------------------------------
    2980 -        def get_loaded_plugins(self, auth_cookie = None):
    2981                  if not self.__attached:
    2982                          return 0
    2983                  if auth_cookie != self.__auth_cookie:
    2984                          _log.error('non-authenticated get_loaded_plugins()')
    2985                          return 0
    2986                  gb = gmGuiBroker.GuiBroker()
    2987                  return gb['horstspace.notebook.gui'].keys()
    2988          #-----------------------------------------------------------------
    2989 -        def raise_notebook_plugin(self, auth_cookie = None, a_plugin = None):
    2990                  """Raise a notebook plugin within GNUmed."""
    2991                  if not self.__attached:
    2992                          return 0
    2993                  if auth_cookie != self.__auth_cookie:
    2994                          _log.error('non-authenticated raise_notebook_plugin()')
    2995                          return 0
    2996                  # FIXME: use semaphore
    2997                  wx.CallAfter(gmPlugin.raise_notebook_plugin, a_plugin)
    2998                  return 1
    2999          #-----------------------------------------------------------------
    3000 -        def load_patient_from_external_source(self, auth_cookie = None):
    3001                  """Load external patient, perhaps create it.
    3002
    3003                  Callers must use get_user_answer() to get status information.
    3004                  It is unsafe to proceed without knowing the completion state as
    3005                  the controlled client may be waiting for user input from a
    3006                  patient selection list.
    3007                  """
    3008                  if not self.__attached:
    3009                          return (0, _('request rejected, you are not attach()ed'))
    3010                  if auth_cookie != self.__auth_cookie:
    3011                          _log.error('non-authenticated load_patient_from_external_source()')
    3012                          return (0, _('rejected load_patient_from_external_source(), not authenticated'))
    3013                  if self.__pat.locked:
    3014                          _log.error('patient is locked, cannot load from external source')
    3015                          return (0, _('current patient is locked'))
    3016                  self.__user_done = False
    3017                  wx.CallAfter(self._load_patient_from_external_source)
    3018                  self.__lock_after_load_cookie = str(random.random())
    3019                  return (1, self.__lock_after_load_cookie)
    3020          #-----------------------------------------------------------------
    3021 -        def lock_loaded_patient(self, auth_cookie = None, lock_after_load_cookie = None):
    3022                  if not self.__attached:
    3023                          return (0, _('request rejected, you are not attach()ed'))
    3024                  if auth_cookie != self.__auth_cookie:
    3025                          _log.error('non-authenticated lock_load_patient()')
    3026                          return (0, _('rejected lock_load_patient(), not authenticated'))
    3027                  # FIXME: ask user what to do about wrong cookie
    3028                  if lock_after_load_cookie != self.__lock_after_load_cookie:
    3029                          _log.warning('patient lock-after-load request rejected due to wrong cookie [%s]' % lock_after_load_cookie)
    3030                          return (0, 'patient lock-after-load request rejected, wrong cookie provided')
    3031                  self.__pat.locked = True
    3032                  self.__pat_lock_cookie = str(random.random())
    3033                  return (1, self.__pat_lock_cookie)
    3034          #-----------------------------------------------------------------
    3035 -        def lock_into_patient(self, auth_cookie = None, search_params = None):
    3036                  if not self.__attached:
    3037                          return (0, _('request rejected, you are not attach()ed'))
    3038                  if auth_cookie != self.__auth_cookie:
    3039                          _log.error('non-authenticated lock_into_patient()')
    3040                          return (0, _('rejected lock_into_patient(), not authenticated'))
    3041                  if self.__pat.locked:
    3042                          _log.error('patient is already locked')
    3043                          return (0, _('already locked into a patient'))
    3044                  searcher = gmPersonSearch.cPatientSearcher_SQL()
    3045                  if type(search_params) == dict:
    3046                          idents = searcher.get_identities(search_dict=search_params)
    3047                          raise Exception("must use dto, not search_dict")
    3048                  else:
    3049                          idents = searcher.get_identities(search_term=search_params)
    3050                  if idents is None:
    3051                          return (0, _('error searching for patient with [%s]/%s') % (search_term, search_dict))
    3052                  if len(idents) == 0:
    3053                          return (0, _('no patient found for [%s]/%s') % (search_term, search_dict))
    3054                  # FIXME: let user select patient
    3055                  if len(idents) > 1:
    3056                          return (0, _('several matching patients found for [%s]/%s') % (search_term, search_dict))
    3057                  if not gmPatSearchWidgets.set_active_patient(patient = idents[0]):
    3058                          return (0, _('cannot activate patient [%s] (%s/%s)') % (str(idents[0]), search_term, search_dict))
    3059                  self.__pat.locked = True
    3060                  self.__pat_lock_cookie = str(random.random())
    3061                  return (1, self.__pat_lock_cookie)
    3062          #-----------------------------------------------------------------
    3063 -        def unlock_patient(self, auth_cookie = None, unlock_cookie = None):
    3064                  if not self.__attached:
    3065                          return (0, _('request rejected, you are not attach()ed'))
    3066                  if auth_cookie != self.__auth_cookie:
    3067                          _log.error('non-authenticated unlock_patient()')
    3068                          return (0, _('rejected unlock_patient, not authenticated'))
    3069                  # we ain't locked anyways, so succeed
    3070                  if not self.__pat.locked:
    3071                          return (1, '')
    3072                  # FIXME: ask user what to do about wrong cookie
    3073                  if unlock_cookie != self.__pat_lock_cookie:
    3074                          _log.warning('patient unlock request rejected due to wrong cookie [%s]' % unlock_cookie)
    3075                          return (0, 'patient unlock request rejected, wrong cookie provided')
    3076                  self.__pat.locked = False
    3077                  return (1, '')
    3078          #-----------------------------------------------------------------
    3079 -        def assume_staff_identity(self, auth_cookie = None, staff_name = "Dr.Jekyll", staff_creds = None):
    3080                  if not self.__attached:
    3081                          return 0
    3082                  if auth_cookie != self.__auth_cookie:
    3083                          _log.error('non-authenticated select_identity()')
    3084                          return 0
    3085                  return "cMacroPrimitives.assume_staff_identity() not implemented"
    3086          #-----------------------------------------------------------------
    3087 -        def get_user_answer(self):
    3088                  if not self.__user_done:
    3089                          return (0, 'still waiting')
    3090                  self.__user_done = False
    3091                  return (1, self.__user_answer)
    3092          #-----------------------------------------------------------------
    3093          # internal API
    3094          #-----------------------------------------------------------------
    3095 -        def _force_detach(self):
    3096                  msg = _(
    3097                          'Someone tries to forcibly break the existing\n'
    3098                          'controlling connection. This may or may not\n'
    3099                          'have legitimate reasons.\n\n'
    3100                          'Do you want to allow breaking the connection ?'
    3101                  )
    3102                  can_break_conn = gmGuiHelpers.gm_show_question (
    3103                          aMessage = msg,
    3104                          aTitle = _('forced detach attempt')
    3105                  )
    3106                  if can_break_conn:
    3107                          self.__user_answer = 1
    3108                  else:
    3109                          self.__user_answer = 0
    3110                  self.__user_done = True
    3111                  if can_break_conn:
    3112                          self.__pat.locked = False
    3113                          self.__attached = 0
    3114                  return 1
    3115          #-----------------------------------------------------------------
    3116 -        def _shutdown_gnumed(self, forced=False):
    3117                  top_win = wx.GetApp().GetTopWindow()
    3118                  if forced:
    3119                          top_win.DestroyLater()
    3120                  else:
    3121                          top_win.Close()
    3122          #-----------------------------------------------------------------
    3123 -        def _load_patient_from_external_source(self):
    3124                  patient = gmPatSearchWidgets.get_person_from_external_sources(search_immediately = True, activate_immediately = True)
    3125                  if patient is not None:
    3126                          self.__user_answer = 1
    3127                  else:
    3128                          self.__user_answer = 0
    3129                  self.__user_done = True
    3130                  return 1
    3131  #=====================================================================
    3132  # main
    3133  #=====================================================================
    3134  if __name__ == '__main__':
    3135
    3136          if len(sys.argv) < 2:
    3137                  sys.exit()
    3138
    3139          if sys.argv[1] != 'test':
    3140                  sys.exit()
    3141
    3142          gmI18N.activate_locale()
    3143          gmI18N.install_domain()
    3144
    3145          #--------------------------------------------------------
    3146 -        def test_placeholders():
    3147                  handler = gmPlaceholderHandler()
    3148                  handler.debug = True
    3149
    3150                  for placeholder in ['a', 'b']:
    3151                          print(handler[placeholder])
    3152
    3153                  pat = gmPersonSearch.ask_for_patient()
    3154                  if pat is None:
    3155                          return
    3156
    3157                  gmPatSearchWidgets.set_active_patient(patient = pat)
    3158
    3159                  print('DOB (YYYY-MM-DD):', handler['date_of_birth::%Y-%m-%d'])
    3160
    3161                  app = wx.PyWidgetTester(size = (200, 50))
    3162
    3163                  ph = 'progress_notes::ap'
    3164                  print('%s: %s' % (ph, handler[ph]))
    3165          #--------------------------------------------------------
    3166 -        def test_new_variant_placeholders():
    3167
    3168                  tests = [
    3169                          # should work:
    3170                          '$<lastname>$',
    3171                          '$<lastname::::3>$',
    3172                          '$<name::%(title)s %(firstnames)s%(preferred)s%(lastnames)s>$',
    3173
    3174                          # should fail:
    3175                          'lastname',
    3176                          '$<lastname',
    3177                          '$<lastname::',
    3178                          '$<lastname::>$',
    3179                          '$<lastname::abc>$',
    3180                          '$<lastname::abc::>$',
    3181                          '$<lastname::abc::3>$',
    3182                          '$<lastname::abc::xyz>$',
    3183                          '$<lastname::::>$',
    3184                          '$<lastname::::xyz>$',
    3185
    3186                          '$<date_of_birth::%Y-%m-%d>$',
    3187                          '$<date_of_birth::%Y-%m-%d::3>$',
    3188                          '$<date_of_birth::%Y-%m-%d::>$',
    3189
    3190                          # should work:
    3191                          '$<adr_location::home::35>$',
    3192                          '$<gender_mapper::male//female//other::5>$',
    3193                          '$<current_meds::==> %(product)s %(preparation)s (%(substance)s) <==\n::50>$',
    3194                          '$<allergy_list::%(descriptor)s, >$',
    3195                          '$<current_meds_table::latex//>$'
    3196
    3197  #                       'firstname',
    3198  #                       'title',
    3199  #                       'date_of_birth',
    3200  #                       'progress_notes',
    3201  #                       'soap',
    3202  #                       'soap_s',
    3203  #                       'soap_o',
    3204  #                       'soap_a',
    3205  #                       'soap_p',
    3206
    3207  #                       'soap',
    3208  #                       'progress_notes',
    3209  #                       'date_of_birth'
    3210                  ]
    3211
    3212  #               tests = [
    3213  #                       '$<latest_vaccs_table::latex>$'
    3214  #               ]
    3215
    3216                  pat = gmPersonSearch.ask_for_patient()
    3217                  if pat is None:
    3218                          return
    3219
    3220                  gmPatSearchWidgets.set_active_patient(patient = pat)
    3221
    3222                  handler = gmPlaceholderHandler()
    3223                  handler.debug = True
    3224
    3225                  for placeholder in tests:
    3226                          print(placeholder, "=>", handler[placeholder])
    3227                          print("--------------")
    3228                          input()
    3229
    3230  #               print 'DOB (YYYY-MM-DD):', handler['date_of_birth::%Y-%m-%d']
    3231
    3232  #               app = wx.PyWidgetTester(size = (200, 50))
    3233
    3234  #               ph = 'progress_notes::ap'
    3235  #               print '%s: %s' % (ph, handler[ph])
    3236
    3237          #--------------------------------------------------------
    3238 -        def test_scripting():
    3239                  from Gnumed.pycommon import gmScriptingListener
    3240                  import xmlrpc.client
    3241
    3242                  listener = gmScriptingListener.cScriptingListener(macro_executor = cMacroPrimitives(personality='unit test'), port=9999)
    3243
    3244                  s = xmlrpc.client.ServerProxy('http://localhost:9999')
    3245                  print("should fail:", s.attach())
    3246                  print("should fail:", s.attach('wrong cookie'))
    3247                  print("should work:", s.version())
    3248                  print("should fail:", s.raise_gnumed())
    3249                  print("should fail:", s.raise_notebook_plugin('test plugin'))
    3250                  print("should fail:", s.lock_into_patient('kirk, james'))
    3251                  print("should fail:", s.unlock_patient())
    3252                  status, conn_auth = s.attach('unit test')
    3253                  print("should work:", status, conn_auth)
    3254                  print("should work:", s.version())
    3255                  print("should work:", s.raise_gnumed(conn_auth))
    3256                  status, pat_auth = s.lock_into_patient(conn_auth, 'kirk, james')
    3257                  print("should work:", status, pat_auth)
    3258                  print("should fail:", s.unlock_patient(conn_auth, 'bogus patient unlock cookie'))
    3259                  print("should work", s.unlock_patient(conn_auth, pat_auth))
    3260                  data = {'firstname': 'jame', 'lastnames': 'Kirk', 'gender': 'm'}
    3261                  status, pat_auth = s.lock_into_patient(conn_auth, data)
    3262                  print("should work:", status, pat_auth)
    3263                  print("should work", s.unlock_patient(conn_auth, pat_auth))
    3264                  print(s.detach('bogus detach cookie'))
    3265                  print(s.detach(conn_auth))
    3266                  del s
    3267
    3268                  listener.shutdown()
    3269          #--------------------------------------------------------
    3270 -        def test_placeholder_regex():
    3271
    3272                  import re as regex
    3273
    3274                  tests = [
    3275                          ' $<lastname>$ ',
    3276                          ' $<lastname::::3>$ ',
    3277
    3278                          # should fail:
    3279                          '$<date_of_birth::%Y-%m-%d>$',
    3280                          '$<date_of_birth::%Y-%m-%d::3>$',
    3281                          '$<date_of_birth::%Y-%m-%d::>$',
    3282
    3283                          '$<adr_location::home::35>$',
    3284                          '$<gender_mapper::male//female//other::5>$',
    3285                          '$<current_meds::==> %(product)s %(preparation)s (%(substance)s) <==\\n::50>$',
    3286                          '$<allergy_list::%(descriptor)s, >$',
    3287
    3288                          '\\noindent Patient: $<lastname>$, $<firstname>$',
    3289                          '$<allergies::%(descriptor)s & %(l10n_type)s & {\\footnotesize %(reaction)s} \tabularnewline \hline >$',
    3290                          '$<current_meds::               \item[%(substance)s] {\\footnotesize (%(product)s)} %(preparation)s %(amount)s%(unit)s: %(schedule)s >$'
    3291                  ]
    3292
    3293                  tests = [
    3294
    3295                          'junk   $<lastname::::3>$                       junk',
    3296                          'junk   $<lastname::abc::3>$            junk',
    3297                          'junk   $<lastname::abc>$                       junk',
    3298                          'junk   $<lastname>$                            junk',
    3299
    3300                          'junk   $<lastname>$   junk   $<firstname>$                                             junk',
    3301                          'junk   $<lastname::abc>$   junk   $<fiststname::abc>$                  junk',
    3302                          'junk   $<lastname::abc::3>$   junk   $<firstname::abc::3>$             junk',
    3303                          'junk   $<lastname::::3>$   junk   $<firstname::::3>$                   junk'
    3304
    3305                  ]
    3306
    3307                  tests = [
    3308  #                       u'junk   $<<<date_of_birth::%Y %B %d $<inner placeholder::%Y %B %d::20>$::20>>>$   junk',
    3309  #                       u'junk   $<date_of_birth::%Y %B %d::20>$   $<date_of_birth::%Y %B %d::20>$',
    3310  #                       u'junk   $<date_of_birth::%Y %B %d::>$   $<date_of_birth::%Y %B %d::20>$   $<<date_of_birth::%Y %B %d::20>>$',
    3311  #                       u'junk   $<date_of_birth::::20>$',
    3312  #                       u'junk   $<date_of_birth::::>$',
    3313                          'junk $<<<current_meds::%(product)s (%(substance)s): Dispense $<free_text::Dispense how many of %(product)s %(preparation)s (%(substance)s) ?::20>$ (%(preparation)s) \\n::>>>$ junk',
    3314                          'junk $<<<current_meds::%(product)s (%(substance)s): Dispense $<free_text::Dispense how many of %(product)s %(preparation)s (%(substance)s) ?::20>$ (%(preparation)s) \\n::250>>>$ junk',
    3315                          'junk $<<<current_meds::%(product)s (%(substance)s): Dispense $<free_text::Dispense how many of %(product)s %(preparation)s (%(substance)s) ?::20>$ (%(preparation)s) \\n::3-4>>>$ junk',
    3316
    3317                          'should fail $<<<current_meds::%(product)s (%(substance)s): Dispense $<free_text::Dispense how many of %(product)s %(preparation)s (%(substance)s) ?::20>$ (%(preparation)s) \\n::->>>$ junk',
    3318                          'should fail $<<<current_meds::%(product)s (%(substance)s): Dispense $<free_text::Dispense how many of %(product)s %(preparation)s (%(substance)s) ?::20>$ (%(preparation)s) \\n::3->>>$ junk',
    3319                          'should fail $<<<current_meds::%(product)s (%(substance)s): Dispense $<free_text::Dispense how many of %(product)s %(preparation)s (%(substance)s) ?::20>$ (%(preparation)s) \\n::-4>>>$ should fail',
    3320                          'should fail $<<<current_meds::%(product)s (%(substance)s): Dispense $<free_text::Dispense how many of %(product)s %(preparation)s (%(substance)s) ?::20>$ (%(preparation)s) \\n::should_fail>>>$ junk',
    3321                          'should fail $<<<current_meds::%(product)s (%(substance)s): Dispense $<free_text::Dispense how many of %(product)s %(preparation)s (%(substance)s) ?::20>$ (%(preparation)s) \\n::should_fail->>>$ junk',
    3322                          'should fail $<<<current_meds::%(product)s (%(substance)s): Dispense $<free_text::Dispense how many of %(product)s %(preparation)s (%(substance)s) ?::20>$ (%(preparation)s) \\n::-should_fail>>>$ junk',
    3323                          'should fail $<<<current_meds::%(product)s (%(substance)s): Dispense $<free_text::Dispense how many of %(product)s %(preparation)s (%(substance)s) ?::20>$ (%(preparation)s) \\n::should_fail-4>>>$ junk',
    3324                          'should fail $<<<current_meds::%(product)s (%(substance)s): Dispense $<free_text::Dispense how many of %(product)s %(preparation)s (%(substance)s) ?::20>$ (%(preparation)s) \\n::3-should_fail>>>$ junk',
    3325                          'should fail $<<<current_meds::%(product)s (%(substance)s): Dispense $<free_text::Dispense how many of %(product)s %(preparation)s (%(substance)s) ?::20>$ (%(preparation)s) \\n::should_fail-should_fail>>>$ junk',
    3326                  ]
    3327
    3328                  tests = [
    3329                          'junk $<<<should pass::template::>>>$ junk',
    3330                          'junk $<<<should pass::template::10>>>$ junk',
    3331                          'junk $<<<should pass::template::10-20>>>$ junk',
    3332                          'junk $<<<should pass::template $<<dummy::template 2::10>>$::>>>$ junk',
    3333                          'junk $<<<should pass::template $<dummy::template 2::10>$::>>>$ junk',
    3334
    3335                          'junk $<<<should pass::template::>>>$ junk $<<<should pass 2::template 2::>>>$ junk',
    3336                          'junk $<<<should pass::template::>>>$ junk $<<should pass 2::template 2::>>$ junk',
    3337                          'junk $<<<should pass::template::>>>$ junk $<should pass 2::template 2::>$ junk',
    3338
    3339                          'junk $<<<should fail::template $<<<dummy::template 2::10>>>$::>>>$ junk',
    3340
    3341                          'junk $<<<should fail::template::10->>>$ junk',
    3342                          'junk $<<<should fail::template::10->>>$ junk',
    3343                          'junk $<<<should fail::template::10->>>$ junk',
    3344                          'junk $<<<should fail::template::10->>>$ junk',
    3345                          'junk $<first_pass::junk $<<<3rd_pass::template::20>>>$ junk::8-10>$ junk'
    3346                  ]
    3347
    3348                  #print "testing placeholder regex:", first_pass_placeholder_regex
    3349                  ##print "testing placeholder regex:", second_pass_placeholder_regex
    3350                  ##print "testing placeholder regex:", third_pass_placeholder_regex
    3351                  #print ""
    3352                  #for t in tests:
    3353                  #       print 'line: "%s"' % t
    3354                  #       phs = regex.findall(first_pass_placeholder_regex, t, regex.IGNORECASE)
    3355                  #       #phs = regex.findall(second_pass_placeholder_regex, t, regex.IGNORECASE)
    3356                  #       #phs = regex.findall(third_pass_placeholder_regex, t, regex.IGNORECASE)
    3357                  #       print " %s placeholders:" % len(phs)
    3358                  #       for p in phs:
    3359                  #               print ' => ', p
    3360                  #       print " "
    3361
    3362                  all_tests = {
    3363                          first_pass_placeholder_regex: [
    3364                                  # different lengths/regions
    3365                                  ('junk $<first_level::template::>$ junk', ['$<first_level::template::>$']),
    3366                                  ('junk $<first_level::template::10>$ junk', ['$<first_level::template::10>$']),
    3367                                  ('junk $<first_level::template::10-12>$ junk', ['$<first_level::template::10-12>$']),
    3368
    3369                                  # inside is other-level:
    3370                                  ('junk $<first_level::$<<insert::insert_template::0>>$::10-12>$ junk', ['$<first_level::$<<insert::insert_template::0>>$::10-12>$']),
    3371                                  ('junk $<first_level::$<<<insert::insert_template::0>>>$::10-12>$ junk', ['$<first_level::$<<<insert::insert_template::0>>>$::10-12>$']),
    3372
    3373                                  # outside is other-level:
    3374                                  ('junk $<<second_level::$<insert::insert_template::0>$::10-12>>$ junk', ['$<insert::insert_template::0>$']),
    3375                                  ('junk $<<<third_level::$<insert::insert_template::0>$::10-12>>>$ junk', ['$<insert::insert_template::0>$']),
    3376
    3377                                  # other level on same line
    3378                                  ('junk $<first_level 1::template 1::>$ junk $<<second_level 2::template 2::>>$ junk', ['$<first_level 1::template 1::>$']),
    3379                                  ('junk $<first_level 1::template 1::>$ junk $<<<third_level 2::template 2::>>>$ junk', ['$<first_level 1::template 1::>$']),
    3380
    3381                                  # this should produce 2 matches
    3382                                  ('junk $<first_level 1::template 1::>$ junk $<first_level 2::template 2::>$ junk', ['$<first_level 1::template 1::>$', '$<first_level 2::template 2::>$']),
    3383
    3384                                  # this will produce a mismatch, due to illegal nesting of same-level placeholders
    3385                                  ('returns illegal match: junk $<first_level::$<insert::insert_template::0>$::10-12>$ junk', ['$<first_level::$<insert::insert_template::0>$::10-12>$']),
    3386                          ],
    3387                          second_pass_placeholder_regex: [
    3388                                  # different lengths/regions
    3389                                  ('junk $<<second_level::template::>>$ junk', ['$<<second_level::template::>>$']),
    3390                                  ('junk $<<second_level::template::10>>$ junk', ['$<<second_level::template::10>>$']),
    3391                                  ('junk $<<second_level::template::10-12>>$ junk', ['$<<second_level::template::10-12>>$']),
    3392
    3393                                  # inside is other-level:
    3394                                  ('junk $<<second_level::$<insert::insert_template::0>$::10-12>>$ junk', ['$<<second_level::$<insert::insert_template::0>$::10-12>>$']),
    3395                                  ('junk $<<second_level::$<<<insert::insert_template::0>>>$::10-12>>$ junk', ['$<<second_level::$<<<insert::insert_template::0>>>$::10-12>>$']),
    3396
    3397                                  # outside is other-level:
    3398                                  ('junk $<first_level::$<<insert::insert_template::0>>$::10-12>$ junk', ['$<<insert::insert_template::0>>$']),
    3399                                  ('junk $<<<third_level::$<<insert::insert_template::0>>$::10-12>>>$ junk', ['$<<insert::insert_template::0>>$']),
    3400
    3401                                  # other level on same line
    3402                                  ('junk $<first_level 1::template 1::>$ junk $<<second_level 2::template 2::>>$ junk', ['$<<second_level 2::template 2::>>$']),
    3403                                  ('junk $<<second_level 1::template 1::>>$ junk $<<<third_level 2::template 2::>>>$ junk', ['$<<second_level 1::template 1::>>$']),
    3404
    3405                                  # this should produce 2 matches
    3406                                  ('junk $<<second_level 1::template 1::>>$ junk $<<second_level 2::template 2::>>$ junk', ['$<<second_level 1::template 1::>>$', '$<<second_level 2::template 2::>>$']),
    3407
    3408                                  # this will produce a mismatch, due to illegal nesting of same-level placeholders
    3409                                  ('returns illegal match: junk $<<second_level::$<<insert::insert_template::0>>$::10-12>>$ junk', ['$<<second_level::$<<insert::insert_template::0>>$::10-12>>$']),
    3410
    3411                          ],
    3412                          third_pass_placeholder_regex: [
    3413                                  # different lengths/regions
    3414                                  ('junk $<<<third_level::template::>>>$ junk', ['$<<<third_level::template::>>>$']),
    3415                                  ('junk $<<<third_level::template::10>>>$ junk', ['$<<<third_level::template::10>>>$']),
    3416                                  ('junk $<<<third_level::template::10-12>>>$ junk', ['$<<<third_level::template::10-12>>>$']),
    3417
    3418                                  # inside is other-level:
    3419                                  ('junk $<<<third_level::$<<insert::insert_template::0>>$::10-12>>>$ junk', ['$<<<third_level::$<<insert::insert_template::0>>$::10-12>>>$']),
    3420                                  ('junk $<<<third_level::$<insert::insert_template::0>$::10-12>>>$ junk', ['$<<<third_level::$<insert::insert_template::0>$::10-12>>>$']),
    3421
    3422                                  # outside is other-level:
    3423                                  ('junk $<<second_level::$<<<insert::insert_template::0>>>$::10-12>>$ junk', ['$<<<insert::insert_template::0>>>$']),
    3424                                  ('junk $<first_level::$<<<insert::insert_template::0>>>$::10-12>$ junk', ['$<<<insert::insert_template::0>>>$']),
    3425
    3426                                  # other level on same line
    3427                                  ('junk $<first_level 1::template 1::>$ junk $<<<third_level 2::template 2::>>>$ junk', ['$<<<third_level 2::template 2::>>>$']),
    3428                                  ('junk $<<second_level 1::template 1::>>$ junk $<<<third_level 2::template 2::>>>$ junk', ['$<<<third_level 2::template 2::>>>$']),
    3429
    3430                                  # this will produce a mismatch, due to illegal nesting of same-level placeholders
    3431                                  ('returns illegal match: junk $<<<third_level::$<<<insert::insert_template::0>>>$::10-12>>>$ junk', ['$<<<third_level::$<<<insert::insert_template::0>>>$::10-12>>>$']),
    3432                          ]
    3433                  }
    3434
    3435                  for pattern in [first_pass_placeholder_regex, second_pass_placeholder_regex, third_pass_placeholder_regex]:
    3436                          print("")
    3437                          print("-----------------------------")
    3438                          print("regex:", pattern)
    3439                          tests = all_tests[pattern]
    3440                          for t in tests:
    3441                                  line, expected_results = t
    3442                                  phs = regex.findall(pattern, line, regex.IGNORECASE)
    3443                                  if len(phs) > 0:
    3444                                          if phs == expected_results:
    3445                                                  continue
    3446
    3447                                  print("")
    3448                                  print("failed")
    3449                                  print("line:", line)
    3450
    3451                                  if len(phs) == 0:
    3452                                          print("no match")
    3453                                          continue
    3454
    3455                                  if len(phs) > 1:
    3456                                          print("several matches")
    3457                                          for r in expected_results:
    3458                                                  print("expected:", r)
    3459                                          for p in phs:
    3460                                                  print("found:", p)
    3461                                          continue
    3462
    3463                                  print("unexpected match")
    3464                                  print("expected:", expected_results)
    3465                                  print("found:   ", phs)
    3466
    3467          #--------------------------------------------------------
    3468 -        def test_placeholder():
    3469
    3470                  phs = [
    3471                          #u'emr_journal::soapu //%(clin_when)s  %(modified_by)s  %(soap_cat)s  %(narrative)s//1000 days::',
    3472                          #u'free_text::placeholder test//preset::9999',
    3473                          #u'soap_for_encounters:://::9999',
    3474                          #u'soap_p',
    3475                          #u'encounter_list::%(started)s: %(assessment_of_encounter)s::30',
    3476                          #u'patient_comm::homephone::1234',
    3477                          #u'$<patient_address::work::1234>$',
    3478                          #u'adr_region::home::1234',
    3479                          #u'adr_country::fehlt::1234',
    3480                          #u'adr_subunit::fehlt::1234',
    3481                          #u'adr_suburb::fehlt-auch::1234',
    3482                          #u'external_id::Starfleet Serial Number//Star Fleet Central Staff Office::1234',
    3483                          #u'primary_praxis_provider',
    3484                          #u'current_provider::::3-5',
    3485                          #u'current_provider_external_id::Starfleet Serial Number//Star Fleet Central Staff Office::1234',
    3486                          #u'current_provider_external_id::LANR//LÄK::1234'
    3487                          #u'$<current_provider_external_id::KV-LANR//KV::1234>$'
    3488                          #u'primary_praxis_provider_external_id::LANR//LÄK::1234'
    3489                          #u'form_name_long::::1234',
    3490                          #u'form_name_long::::5',
    3491                          #u'form_name_long::::',
    3492                          #u'form_version::::5',
    3493                          #u'$<current_meds::\item %(product)s %(preparation)s (%(substance)s) from %(started)s for %(duration)s as %(schedule)s until %(discontinued)s\\n::250>$',
    3494                          #u'$<vaccination_history::%(date_given)s: %(vaccine)s [%(batch_no)s] %(l10n_indications)s::250>$',
    3495                          #u'$<date_of_birth::%Y %B %d::20>$',
    3496                          #u'$<date_of_birth::%Y %B %d::>$',
    3497                          #u'$<date_of_birth::::20>$',
    3498                          #u'$<date_of_birth::::>$',
    3499                          #u'$<patient_tags::Tag "%(l10n_description)s": %(comment)s//\\n- ::250>$',
    3500                          #u'$<PHX::%(description)s\n  side: %(laterality)s, active: %(is_active)s, relevant: %(clinically_relevant)s, caused death: %(is_cause_of_death)s//\n//%Y %B %d//latex::250>$',
    3501                          #u'$<patient_photo::\includegraphics[width=60mm]{%s}//image/png//.png::250>$',
    3502                          #u'$<data_snippet::binary_test_snippet//path=<%s>//image/png//.png::250>$',
    3503                          #u'$<data_snippet::autograph-LMcC//path=<%s>//image/jpg//.jpg::250>$',
    3504                          #u'$<current_meds::%s ($<lastname::::50>$)//select::>$',
    3505                          #u'$<current_meds::%s//select::>$',
    3506                          #u'$<soap_by_issue::soapu //%Y %b %d//%(narrative)s::1000>$',
    3507                          #u'$<soap_by_episode::soapu //%Y %b %d//%(narrative)s::1000>$',
    3508                          #u'$<documents::select//description//document %(clin_when)s: %(l10n_type)s// file: %(fullpath)s (<some path>/%(name)s)//~/gnumed/export/::>$',
    3509                          #u'$<soap::soapu //%s::9999>$',
    3510                          #u'$<soap::soapu //%(soap_cat)s: %(date)s | %(provider)s | %(narrative)s::9999>$'
    3511                          #u'$<test_results:://%c::>$'
    3512                          #u'$<test_results::%(unified_abbrev)s: %(unified_val)s %(val_unit)s//%c::>$'
    3513                          '$<most_recent_test_results::tmpl=%(unified_name)s & %(unified_val)s%(val_unit)s & [%(unified_target_min)s--%(unified_target_max)s] %(unified_target_range)s & %(clin_when)s \\tabularnewline::>$'              #<dfmt=...>//<tmpl=...>//<sep=...>
    3514                          #u'$<reminders:://::>$'
    3515                          #u'$<current_meds_for_rx::%(product)s (%(contains)s): dispense %(amount2dispense)s ::>$'
    3516                          #u'$<praxis::%(branch)s (%(praxis)s)::>$'
    3517                          #u'$<praxis_address::::120>$'
    3518                          #u'$<praxis_id::::120>$'
    3519                          #u'$<gen_adr_street::Street = %s//Wählen Sie die Empfängeradresse !::120>$', u'$<gen_adr_location::Ort = %s::120>$', u'$<gen_adr_country::::120>$'
    3520
    3521                          #u'$<receiver_name::%s::120>$',
    3522                          #u'$<receiver_street::%s//a::120>$',
    3523                          #u'$<receiver_number:: %s//a::120>$',
    3524                          #u'$<receiver_subunit:: %s::120>$',
    3525                          #u'$<receiver_postcode::%s//b::120>$',
    3526                          #u'$<receiver_location:: %s::120>$',
    3527                          #u'$<receiver_country::, %s::120>$',
    3528                          #u'$<external_care::%(issue)s: %(provider)s of %(unit)s@%(organization)s (%(comment)s)::1024>$',
    3529                          #u'$<url_escape::hello world ü::>$',
    3530                          #u'$<substance_abuse::%(substance)s (%(harmful_use_type)s) last=%(last_checked_when)s stop=%(discontinued)s // %(notes)s::>$',
    3531                          #u'bill_adr_region::region %s::1234',
    3532                          #u'bill_adr_country::%s::1234',
    3533                          #u'bill_adr_subunit::subunit: %s::1234',
    3534                          #u'bill_adr_suburb::-> %s::1234',
    3535                          #u'bill_adr_street::::1234',
    3536                          #u'bill_adr_number::%s::1234',
    3537                          #u'$<diagnoses::\listitem %s::>$'
    3538                          #u'$<patient_mcf::fmt=txt//card=%s::>$',
    3539                          #u'$<patient_mcf::fmt=mcf//mcf=%s::>$',
    3540                          #u'$<patient_mcf::fmt=qr//png=%s::>$'
    3541                          #u'$<praxis_scan2pay::fmt=txt::>$',
    3542                          #u'$<praxis_scan2pay::fmt=qr::>$'
    3543                          #u'$<bill_scan2pay::fmt=txt::>$',
    3544                          #u'$<bill_scan2pay::fmt=qr::>$'
    3545                  ]
    3546
    3547                  handler = gmPlaceholderHandler()
    3548                  handler.debug = True
    3549
    3550                  gmStaff.set_current_provider_to_logged_on_user()
    3551                  gmPraxisWidgets.set_active_praxis_branch(no_parent = True)
    3552                  pat = gmPersonSearch.ask_for_patient()
    3553                  if pat is None:
    3554                          return
    3555                  gmPatSearchWidgets.set_active_patient(patient = pat)
    3556
    3557                  #app = wx.PyWidgetTester(size = (200, 50))
    3558                  #handler.set_placeholder('form_name_long', 'ein Testformular')
    3559                  for ph in phs:
    3560                          print(ph)
    3561                          print(" result:")
    3562                          print('  %s' % handler[ph])
    3563                  #handler.unset_placeholder('form_name_long')
    3564
    3565          #--------------------------------------------------------
    3566 -        def test():
    3567                  pat = gmPersonSearch.ask_for_patient()
    3568                  if pat is None:
    3569                          sys.exit()
    3570                  gmPerson.set_active_patient(patient = pat)
    3571                  from Gnumed.wxpython import gmMedicationWidgets
    3572                  gmMedicationWidgets.manage_substance_intakes()
    3573
    3574          #--------------------------------------------------------
    3575 -        def test_show_phs():
    3576                  show_placeholders()
    3577
    3578          #--------------------------------------------------------
    3579 -        def test_parse_ph_options(option_defs=None, options_string=None):
    3580                  """Returns a tuple of values in the order of option_defs.keys()."""
    3581                  assert (option_defs is not None), '<option_defs> must not be None'
    3582                  assert (options_string is not None), '<options_string> must not be None'
    3583
    3584                  _log.debug('parsing ::%s:: for %s', options_string, option_defs)
    3585                  options2return = option_defs.copy()
    3586                  options = options_string.split('//')
    3587                  for opt in options:
    3588                          opt_name, opt_val = opt.split('=', 1)
    3589                          if opt_name.strip() not in option_defs.keys():
    3590                                  continue
    3591                          options2return[opt_name] = opt_val
    3592
    3593                  return tuple([ options2return[o_name] for o_name in options2return.keys() ])
    3594
    3595          #--------------------------------------------------------
    3596          #a,b,c = test_parse_ph_options(option_defs = {'opt1': 1, 'opt2': 'two', 'opt4': 'vier'}, options_string = 'opt1=one//opt2=2//opt3=drei')
    3597          #sys.exit()
    3598
    3599          app = wx.App()
    3600
    3601          #test_placeholders()
    3602          #test_new_variant_placeholders()
    3603          #test_scripting()
    3604          #test_placeholder_regex()
    3605          #test()
    3606          test_placeholder()
    3607          #test_show_phs()
    3608