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