Gnumed.wxpython.gmMacro

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

# Source Code for Module Gnumed.wxpython.gmMacro

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