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