1
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
40 from Gnumed.business import gmPerson
41 from Gnumed.business import gmStaff
42 from Gnumed.business import gmDemographicRecord
43 from Gnumed.business import gmMedication
44 from Gnumed.business import gmPathLab
45 from Gnumed.business import gmPersonSearch
46 from Gnumed.business import gmVaccination
47 from Gnumed.business import gmKeywordExpansion
48 from Gnumed.business import gmPraxis
49
50 from Gnumed.wxpython import gmGuiHelpers
51 from Gnumed.wxpython import gmNarrativeWorkflows
52 from Gnumed.wxpython import gmPatSearchWidgets
53 from Gnumed.wxpython import gmPersonContactWidgets
54 from Gnumed.wxpython import gmPlugin
55 from Gnumed.wxpython import gmEMRStructWidgets
56 from Gnumed.wxpython import gmEncounterWidgets
57 from Gnumed.wxpython import gmListWidgets
58 from Gnumed.wxpython import gmDemographicsWidgets
59 from Gnumed.wxpython import gmDocumentWidgets
60 from Gnumed.wxpython import gmKeywordExpansionWidgets
61 from Gnumed.wxpython import gmPraxisWidgets
62 from Gnumed.wxpython import gmAddressWidgets
63
64
65 _log = logging.getLogger('gm.scripting')
66 _cfg = gmCfg2.gmCfgData()
67
68
69
70
71
72
73 known_injectable_placeholders = [
74 'form_name_long',
75 'form_name_short',
76 'form_version',
77 'form_version_internal',
78 'form_last_modified'
79 ]
80
81
82 __known_variant_placeholders = {
83
84 'free_text': """show a dialog for entering some free text:
85 args: <message> shown in input dialog, must not contain either
86 of '::' and whatever the arguments divider is set to (default '//'),
87 will cache input per <message>""",
88
89 'text_snippet': """a text snippet, taken from the keyword expansion mechanism:
90 args: <snippet name>//<template>""",
91
92 'data_snippet': """a binary snippet, taken from the keyword expansion mechanism:
93 args: <snippet name>//<template>//<optional target mime type>//<optional target extension>
94 returns full path to an exported copy of the
95 data rather than the data itself,
96 template: string template for outputting the path
97 target mime type: a mime type into which to convert the image, no conversion if not given
98 target extension: target file name extension, derived from target mime type if not given
99 """,
100
101
102 'range_of': """select range of enclosed text (note that this cannot take into account non-length characters such as enclosed LaTeX code
103 args: <enclosed text>
104 """,
105 'if_not_empty': """format text based on template if not empty
106 args: <possibly-empty-text>//<template-if-not-empty>//<alternative-text-if-empty>
107 """,
108
109 'ph_cfg': """Set placeholder handler options.
110 args: option name//option value//macro return string
111 option names:
112 ellipsis: what to use as ellipsis (if anything) when
113 shortening strings or string regions, setting the
114 value to NONE will switch off ellipis handling,
115 default is switched off
116 argumentsdivider: what to use as divider when splitting
117 an argument string into parts, default is '//',
118 note that the 'config' placeholder will ALWAYS
119 use '//' to split its argument string, regardless
120 of which setting of <argumentsdivider> is in effect,
121 use DEFAULT to reset this setting back to the
122 default '//'
123 encoding: the encoding in which data emitted by GNUmed
124 as placeholder replacement needs to be valid in,
125 note that GNUmed will still emit unicode to replacement
126 consumers but it will ensure the data emitted _can_
127 be encoded by this target encoding (by roundtripping
128 unicode-encoding-unicode)
129 valid from where this placeholder is located at
130 until further change,
131 use DEFAULT to reset encoding back to the default
132 which is to not ensure compatibility,
133 if the encoding ends in '-strict' then the placeholder
134 replacement will fail if the roundtrip fails
135 """,
136
137 'tex_escape': "args: string to escape, mostly obsolete now",
138
139 'url_escape': """Escapes a string suitable for use as _data_ in an URL
140 args: text to escape
141 """,
142
143 'today': "args: strftime format",
144
145 'gender_mapper': """maps gender of patient to a string:
146 args: <value when person is male> // <is female> // <is other>
147 eg. 'male//female//other'
148 or: 'Lieber Patient//Liebe Patientin'""",
149 'client_version': "the version of the current client as a string (no 'v' in front)",
150
151 'gen_adr_street': """part of a generic address, cached, selected from database:
152 args: optional template//optional selection message//optional cache ID
153 template: %s-style formatting template
154 message: text message shown in address selection list
155 cache ID: used to differentiate separate cached invocations of this placeholder
156 """,
157 'gen_adr_number': """part of a generic address, cached, selected from database:
158 args: optional template//optional selection message//optional cache ID
159 template: %s-style formatting template
160 message: text message shown in address selection list
161 cache ID: used to differentiate separate cached invocations of this placeholder
162 """,
163 'gen_adr_subunit': """part of a generic address, cached, selected from database:
164 args: optional template//optional selection message//optional cache ID
165 template: %s-style formatting template
166 message: text message shown in address selection list
167 cache ID: used to differentiate separate cached invocations of this placeholder
168 """,
169 'gen_adr_location': """part of a generic address, cached, selected from database:
170 args: optional template//optional selection message//optional cache ID
171 template: %s-style formatting template
172 message: text message shown in address selection list
173 cache ID: used to differentiate separate cached invocations of this placeholder
174 """,
175 'gen_adr_suburb': """part of a generic address, cached, selected from database:
176 args: optional template//optional selection message//optional cache ID
177 template: %s-style formatting template
178 message: text message shown in address selection list
179 cache ID: used to differentiate separate cached invocations of this placeholder
180 """,
181 'gen_adr_postcode': """part of a generic address, cached, selected from database:
182 args: optional template//optional selection message//optional cache ID
183 template: %s-style formatting template
184 message: text message shown in address selection list
185 cache ID: used to differentiate separate cached invocations of this placeholder
186 """,
187 'gen_adr_region': """part of a generic address, cached, selected from database:
188 args: optional template//optional selection message//optional cache ID
189 template: %s-style formatting template
190 message: text message shown in address selection list
191 cache ID: used to differentiate separate cached invocations of this placeholder
192 """,
193 'gen_adr_country': """part of a generic address, cached, selected from database:
194 args: optional template//optional selection message//optional cache ID
195 template: %s-style formatting template
196 message: text message shown in address selection list
197 cache ID: used to differentiate separate cached invocations of this placeholder
198 """,
199
200 'receiver_name': """the receiver name, cached, selected from database:
201 receivers are presented for selection from people/addresses related
202 to the patient in some way or other,
203 args: optional template//optional cache ID
204 template: %s-style formatting template
205 cache ID: used to differentiate separate cached invocations of this placeholder
206 """,
207 'receiver_street': """part of a receiver address, cached, selected from database:
208 receivers are presented for selection from people/addresses related
209 to the patient in some way or other,
210 args: optional template//optional cache ID
211 template: %s-style formatting template
212 cache ID: used to differentiate separate cached invocations of this placeholder
213 """,
214 'receiver_number': """part of a receiver address, cached, selected from database:
215 receivers are presented for selection from people/addresses related
216 to the patient in some way or other,
217 args: optional template//optional cache ID
218 template: %s-style formatting template
219 cache ID: used to differentiate separate cached invocations of this placeholder
220 """,
221 'receiver_subunit': """part of a receiver address, cached, selected from database:
222 receivers are presented for selection from people/addresses related
223 to the patient in some way or other,
224 args: optional template//optional cache ID
225 template: %s-style formatting template
226 cache ID: used to differentiate separate cached invocations of this placeholder
227 """,
228 'receiver_location': """part of a receiver address, cached, selected from database:
229 receivers are presented for selection from people/addresses related
230 to the patient in some way or other,
231 args: optional template//optional cache ID
232 template: %s-style formatting template
233 cache ID: used to differentiate separate cached invocations of this placeholder
234 """,
235 'receiver_suburb': """part of a receiver address, cached, selected from database:
236 receivers are presented for selection from people/addresses related
237 to the patient in some way or other,
238 args: optional template//optional cache ID
239 template: %s-style formatting template
240 cache ID: used to differentiate separate cached invocations of this placeholder
241 """,
242 'receiver_postcode': """part of a receiver address, cached, selected from database:
243 receivers are presented for selection from people/addresses related
244 to the patient in some way or other,
245 args: optional template//optional cache ID
246 template: %s-style formatting template
247 cache ID: used to differentiate separate cached invocations of this placeholder
248 """,
249 'receiver_region': """part of a receiver address, cached, selected from database:
250 receivers are presented for selection from people/addresses related
251 to the patient in some way or other,
252 args: optional template//optional cache ID
253 template: %s-style formatting template
254 cache ID: used to differentiate separate cached invocations of this placeholder
255 """,
256 'receiver_country': """part of a receiver address, cached, selected from database:
257 receivers are presented for selection from people/addresses related
258 to the patient in some way or other,
259 args: optional template//optional cache ID
260 template: %s-style formatting template
261 cache ID: used to differentiate separate cached invocations of this placeholder
262 """,
263
264
265 'name': "args: template for name parts arrangement",
266 'date_of_birth': "args: strftime date/time format directive",
267
268 'patient_address': "args: <type of address>//<optional formatting template>",
269 'adr_street': "args: <type of address>, cached per type",
270 'adr_number': "args: <type of address>, cached per type",
271 'adr_subunit': "args: <type of address>, cached per type",
272 'adr_location': "args: <type of address>, cached per type",
273 'adr_suburb': "args: <type of address>, cached per type",
274 'adr_postcode': "args: <type of address>, cached per type",
275 'adr_region': "args: <type of address>, cached per type",
276 'adr_country': "args: <type of address>, cached per type",
277
278 'patient_comm': "args: <comm channel type as per database>//<%(field)s-template>",
279
280 'patient_vcf': """returns path to VCF for current patient
281 args: <template>
282 template: %s-template for path
283 """,
284 'patient_gdt': """returns path to GDT for current patient
285 args: <template>
286 template: %s-template for path
287 """,
288
289 'patient_tags': "args: <%(field)s-template>//<separator>",
290
291 'patient_photo': """outputs URL to exported patient photo (cached per mime type and extension):
292 args: <template>//<optional target mime type>//<optional target extension>,
293 returns full path to an exported copy of the
294 image rather than the image data itself,
295 returns u'' if no mugshot available,
296 template: string template for outputting the path
297 target mime type: a mime type into which to convert the image, no conversion if not given
298 target extension: target file name extension, derived from target mime type if not given""",
299 'external_id': "args: <type of ID>//<issuer of ID>",
300
301
302
303 'soap': "get all of SOAPU/ADMIN, no template in args needed",
304 'soap_s': "get subset of SOAPU/ADMIN, no template in args needed",
305 'soap_o': "get subset of SOAPU/ADMIN, no template in args needed",
306 'soap_a': "get subset of SOAPU/ADMIN, no template in args needed",
307 'soap_p': "get subset of SOAPU/ADMIN, no template in args needed",
308 'soap_u': "get subset of SOAPU/ADMIN, no template in args needed",
309 'soap_admin': "get subset of SOAPU/ADMIN, no template in args needed",
310
311 'progress_notes': """get progress notes:
312 args: categories//template
313 categories: string with 'soapu '; ' ' == None == admin
314 template: u'something %s something' (do not include // in template !)""",
315
316 'soap_for_encounters': """lets the user select a list of encounters for which:
317 LaTeX formatted progress notes are emitted,
318 args: soap categories // strftime date format""",
319
320 'soap_by_issue': """lets the user select a list of issues and then SOAP entries from those issues:
321 args: soap categories // strftime date format // template""",
322
323 'soap_by_episode': """lets the user select a list of episodes and then SOAP entries from those episodes:
324 args: soap categories // strftime date format // template""",
325
326 'emr_journal': """returns EMR journal view entries:
327 args format: <categories>//<template>//<line length>//<time range>//<target format>
328 categories: string with any of "s", "o", "a", "p", "u", " "; (" " == None == admin category)
329 template: something %s something else (Do not include // in the template !)
330 line length: the maximum length of individual lines, not the total placeholder length
331 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)""",
332
333 'substance_abuse': """returns substance abuse entries:
334 args: line template
335 """,
336
337 'current_meds': """returns current medications:
338 args: line template//<select>
339 <select>: if this is present the user will be asked which meds to export""",
340
341 'current_meds_for_rx': """formats substance intakes either by substance (non-product intakes) or by producdt (once per product intake, even if multi-component):
342 args: <line template>
343 <line_template>: template into which to insert each intake, keys from
344 clin.v_substance_intakes, special additional keys:
345 %(contains)s -- list of components
346 %(amount2dispense)s -- how much/many to dispense""",
347
348 'current_meds_AMTS': """emit LaTeX longtable lines with appropriate page breaks:
349 also creates per-page AMTS QR codes and sets the
350 following internal placeholders:
351 amts_png_file_1
352 amts_png_file_2
353 amts_png_file_3
354 amts_data_file_1
355 amts_data_file_2
356 amts_data_file_3
357 amts_png_file_current_page
358 amts_data_file_utf8
359 amts_png_file_utf8
360 the last of which contains the LaTeX command \\thepage (such that
361 LaTeX can use this in, say, page headers) but omitting the .png
362 (for which LaTeX will look by itself),
363 note that you will have to use the 2nd- or 3rd-pass placeholder
364 format if you plan to insert the above because they will only be
365 available by first (or second) pass processing of the initial
366 placeholder "current_meds_AMTS"
367 """,
368 'current_meds_AMTS_enhanced': """emit LaTeX longtable lines with appropriate page breaks:
369 this returns the same content as current_meds_AMTS except that
370 it does not truncate output data whenever possible
371 """,
372
373 'current_meds_table': "emits a LaTeX table, no arguments",
374 'current_meds_notes': "emits a LaTeX table, no arguments",
375 'lab_table': "emits a LaTeX table, no arguments",
376 'test_results': "args: <%(field)s-template>//<date format>//<line separator (EOL)>",
377 'latest_vaccs_table': "emits a LaTeX table, no arguments",
378 'vaccination_history': "args: <%(field)s-template//date format> to format one vaccination per line",
379 'allergy_state': "no arguments",
380 'allergies': "args: line template, one allergy per line",
381 'allergy_list': "args holds: template per allergy, all allergies on one line",
382 'problems': "args holds: line template, one problem per line",
383 'diagnoses': 'args: line template, one diagnosis per line',
384 'PHX': "Past medical HiXtory; args: line template//separator//strftime date format",
385 'encounter_list': "args: per-encounter template, each ends up on one line",
386
387 'documents': """retrieves documents from the archive:
388 args: <select>//<description>//<template>//<path template>//<path>
389 select: let user select which documents to include, optional, if not given: all documents included
390 description: whether to include descriptions, optional
391 template: something %(field)s something else (do not include '//' or '::' itself in the template)
392 path template: the template for outputting the path to exported
393 copies of the document pages, if not given no pages are exported,
394 this template can contain "%(name)s" and/or "%(fullpath)s" which
395 is replaced by the appropriate value for each exported file
396 path: into which path to export copies of the document pages, temp dir if not given""",
397
398 'reminders': """patient reminders:
399 args: <template>//<date format>
400 template: something %(field)s something else (do not include '//' or '::' itself in the template)""",
401
402 'external_care': """External care entries:
403 args: <template>
404 template: something %(field)s something else (do not include '//' or '::' itself in the template)""",
405
406
407 'current_provider': "no arguments",
408 'current_provider_name': """formatted name of current provider:
409 args: <template>,
410 template: something %(field)s something else (do not include '//' or '::' itself in the template)
411 """,
412 'current_provider_title': """formatted name of current provider:
413 args: <optional template>,
414 template: something %(title)s something else (do not include '//' or '::' itself in the template)
415 """,
416 'current_provider_firstnames': """formatted name of current provider:
417 args: <optional template>,
418 template: something %(firstnames)s something else (do not include '//' or '::' itself in the template)
419 """,
420 'current_provider_lastnames': """formatted name of current provider:
421 args: <optional template>,
422 template: something %(lastnames)s something else (do not include '//' or '::' itself in the template)
423 """,
424 'current_provider_external_id': "args: <type of ID>//<issuer of ID>",
425 'primary_praxis_provider': "primary provider for current patient in this praxis",
426 'primary_praxis_provider_external_id': "args: <type of ID>//<issuer of ID>",
427
428
429
430 'praxis': """retrieve current branch of your praxis:
431 args: <template>//select
432 template: something %(field)s something else (do not include '//' or '::' itself in the template)
433 select: if this is present allow selection of the branch rather than using the current branch""",
434
435 'praxis_address': "args: <optional formatting template>",
436 'praxis_comm': "args: type//<optional formatting template>",
437 'praxis_id': "args: <type of ID>//<issuer of ID>//<optional formatting template>",
438 'praxis_vcf': """returns path to VCF for current praxis branch
439 args: <template>
440 template: %s-template for path
441 """,
442
443
444 'bill': """retrieve a bill
445 args: <template>//<date format>
446 template: something %(field)s something else (do not include '//' or '::' itself in the template)
447 date format: strftime date format""",
448 'bill_item': """retrieve the items of a previously retrieved (and therefore cached until the next retrieval) bill
449 args: <template>//<date format>
450 template: something %(field)s something else (do not include '//' or '::' itself in the template)
451 date format: strftime date format""",
452 'bill_adr_street': "args: optional template (%s-style formatting template); cached per bill",
453 'bill_adr_number': "args: optional template (%s-style formatting template); cached per bill",
454 'bill_adr_subunit': "args: optional template (%s-style formatting template); cached per bill",
455 'bill_adr_location': "args: optional template (%s-style formatting template); cached per bill",
456 'bill_adr_suburb': "args: optional template (%s-style formatting template); cached per bill",
457 'bill_adr_postcode': "args: optional template (%s-style formatting template); cached per bill",
458 'bill_adr_region': "args: optional template (%s-style formatting template); cached per bill",
459 'bill_adr_country': "args: optional template (%s-style formatting template); cached per bill"
460
461 }
462
463 known_variant_placeholders = __known_variant_placeholders.keys()
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482 default_placeholder_regex = r'\$<[^<:]+::.*?::\d*?>\$|\$<[^<:]+::.*?::\d+-\d+>\$'
483 first_pass_placeholder_regex = r'|'.join ([
484 r'\$<[^<:]+::.*?(?=::\d*?>\$)::\d*?>\$',
485 r'\$<[^<:]+::.*?(?=::\d+-\d+>\$)::\d+-\d+>\$'
486 ])
487 second_pass_placeholder_regex = r'|'.join ([
488 r'\$<<[^<:]+?::.*?(?=::\d*?>>\$)::\d*?>>\$',
489 r'\$<<[^<:]+?::.*?(?=::\d+-\d+>>\$)::\d+-\d+>>\$'
490 ])
491 third_pass_placeholder_regex = r'|'.join ([
492 r'\$<<<[^<:]+?::.*?(?=::\d*?>>>\$)::\d*?>>>\$',
493 r'\$<<<[^<:]+?::.*?(?=::\d+-\d+>>>\$)::\d+-\d+>>>\$'
494 ])
495
496 default_placeholder_start = '$<'
497 default_placeholder_end = '>$'
498
499
501
502 fname = gmTools.get_unique_filename(prefix = 'gm-placeholders-', suffix = '.txt')
503 ph_file = io.open(fname, mode = 'wt', encoding = 'utf8', errors = 'replace')
504
505 ph_file.write('Here you can find some more documentation on placeholder use:\n')
506 ph_file.write('\n http://wiki.gnumed.de/bin/view/Gnumed/GmManualLettersForms\n\n\n')
507
508 ph_file.write('Variable placeholders:\n')
509 ph_file.write('Usage: $<PLACEHOLDER_NAME::ARGUMENTS::REGION_DEFINITION>$)\n')
510 ph_file.write(' REGION_DEFINITION:\n')
511 ph_file.write('* a single number specifying the maximum output length or\n')
512 ph_file.write('* a number, a "-", followed by a second number specifying the region of the string to return\n')
513 ph_file.write('ARGUMENTS:\n')
514 ph_file.write('* depend on the actual placeholder (see there)\n')
515 ph_file.write('* if a template is supported it will be used to %-format the output\n')
516 ph_file.write('* templates may be either %s-style or %(name)s-style\n')
517 ph_file.write('* templates cannot contain "::"\n')
518 ph_file.write('* templates cannot contain whatever the arguments divider is set to (default "//")\n')
519 for ph in known_variant_placeholders:
520 txt = __known_variant_placeholders[ph]
521 ph_file.write('\n')
522 ph_file.write(' ---=== %s ===---\n' % ph)
523 ph_file.write('\n')
524 ph_file.write(txt)
525 ph_file.write('\n\n')
526 ph_file.write('\n')
527
528 ph_file.write('Known injectable placeholders (use like: $<PLACEHOLDER_NAME::ARGUMENTS::MAX OUTPUT LENGTH>$):\n')
529 for ph in known_injectable_placeholders:
530 ph_file.write(' %s\n' % ph)
531 ph_file.write('\n')
532
533 ph_file.close()
534 gmMimeLib.call_viewer_on_file(aFile = fname, block = False)
535
536
538 """Returns values for placeholders.
539
540 - patient related placeholders operate on the currently active patient
541 - is passed to the forms handling code, for example
542
543 Return values when .debug is False:
544 - errors with placeholders return None
545 - placeholders failing to resolve to a value return an empty string
546
547 Return values when .debug is True:
548 - errors with placeholders return an error string
549 - placeholders failing to resolve to a value return a warning string
550
551 There are several types of placeholders:
552
553 injectable placeholders
554 - they must be set up before use by set_placeholder()
555 - they should be removed after use by unset_placeholder()
556 - the syntax is like extended static placeholders
557 - known ones are listed in known_injectable_placeholders
558 - per-form ones can be used but must exist before
559 the form is processed
560
561 variant placeholders
562 - those are listed in known_variant_placeholders
563 - they are parsed into placeholder, data, and maximum length
564 - the length is optional
565 - data is passed to the handler
566
567 Note that this cannot be called from a non-gui thread unless
568 wrapped in wx.CallAfter().
569 """
571
572 self.pat = gmPerson.gmCurrentPatient()
573 self.debug = False
574
575 self.invalid_placeholder_template = _('invalid placeholder >>>>>%s<<<<<')
576
577 self.__injected_placeholders = {}
578 self.__cache = {}
579
580 self.__esc_style = None
581 self.__esc_func = lambda x:x
582
583 self.__ellipsis = None
584 self.__args_divider = '//'
585 self.__data_encoding = None
586 self.__data_encoding_strict = False
587
588
589
590
592 _log.debug('setting [%s]', key)
593 try:
594 known_injectable_placeholders.index(key)
595 except ValueError:
596 _log.debug('injectable placeholder [%s] unknown', key)
597 if known_only:
598 raise
599 self.__injected_placeholders[key] = value
600
601
603 _log.debug('unsetting [%s]', key)
604 try:
605 del self.__injected_placeholders[key]
606 except KeyError:
607 _log.debug('injectable placeholder [%s] unknown', key)
608
609
611 self.__cache[key] = value
612
614 del self.__cache[key]
615
616
620
621 escape_style = property(lambda x:x, _set_escape_style)
622
623
632
633 escape_function = property(lambda x:x, _set_escape_function)
634
635
640
641 ellipsis = property(lambda x: self.__ellipsis, _set_ellipsis)
642
643
645 if divider == 'DEFAULT':
646 divider = '//'
647 self.__args_divider = divider
648
649 arguments_divider = property(lambda x: self.__args_divider, _set_arguments_divider)
650
651
653 if encoding == 'NONE':
654 self.__data_encoding = None
655 self.__data_encoding_strict = False
656
657 self.__data_encoding_strict = False
658 if encoding.endswith('-strict'):
659 self.__data_encoding_strict = True
660 encoding = encoding[:-7]
661 try:
662 codecs.lookup(encoding)
663 self.__data_encoding = encoding
664 except LookupError:
665 _log.error('<codecs> module can NOT handle encoding [%s]' % enc)
666
667 data_encoding = property(lambda x: self.__data_encoding, _set_data_encoding)
668
669
670 placeholder_regex = property(lambda x: default_placeholder_regex, lambda x:x)
671
672 first_pass_placeholder_regex = property(lambda x: first_pass_placeholder_regex, lambda x:x)
673 second_pass_placeholder_regex = property(lambda x: second_pass_placeholder_regex, lambda x:x)
674 third_pass_placeholder_regex = property(lambda x: third_pass_placeholder_regex, lambda x:x)
675
676
678 region_str = region_str.strip()
679
680 if region_str == '':
681 return None, None
682
683 try:
684 pos_last_char = int(region_str)
685 return 0, pos_last_char
686 except (TypeError, ValueError):
687 _log.debug('region definition not a simple length')
688
689
690 first_last = region_str.split('-')
691 if len(first_last) != 2:
692 _log.error('invalid placeholder region definition: %s', region_str)
693 raise ValueError
694
695 try:
696 pos_first_char = int(first_last[0].strip())
697 pos_last_char = int(first_last[1].strip())
698 except (TypeError, ValueError):
699 _log.error('invalid placeholder region definition: %s', region_str)
700 raise ValueError
701
702
703 if pos_first_char > 0:
704 pos_first_char -= 1
705
706 return pos_first_char, pos_last_char
707
708
710 if self.__data_encoding is None:
711 return data_str
712
713 try:
714 codecs.encode(data_str, self.__data_encoding, 'strict')
715 return data_str
716 except UnicodeEncodeError:
717 _log.error('cannot strict-encode string into [%s]: %s', self.__data_encoding, data_str)
718
719 if self.__data_encoding_strict:
720 return 'not compatible with encoding [%s]: %s' % (self.__data_encoding, data_str)
721
722 try:
723 import unidecode
724 except ImportError:
725 _log.debug('cannot transliterate, <unidecode> module not installed')
726 return codecs.encode(data_str, self.__data_encoding, 'replace').decode(self.__data_encoding)
727
728 return unidecode.unidecode(data_str).decode('utf8')
729
730
731
732
734 """Map self['placeholder'] to self.placeholder.
735
736 This is useful for replacing placeholders parsed out
737 of documents as strings.
738
739 Unknown/invalid placeholders still deliver a result but
740 it will be glaringly obvious if debugging is enabled.
741 """
742 _log.debug('replacing [%s]', placeholder)
743
744 original_placeholder_def = placeholder
745
746
747 if placeholder.startswith(default_placeholder_start):
748 placeholder = placeholder.lstrip('$').lstrip('<')
749 if placeholder.endswith(default_placeholder_end):
750 placeholder = placeholder.rstrip('$').rstrip('>')
751 else:
752 _log.error('placeholder must either start with [%s] and end with [%s] or neither of both', default_placeholder_start, default_placeholder_end)
753 if self.debug:
754 return self._escape(self.invalid_placeholder_template % original_placeholder_def)
755 return None
756
757
758 parts = placeholder.split('::::', 1)
759 if len(parts) == 2:
760 ph_name, region_str = parts
761 is_an_injectable = True
762 try:
763 val = self.__injected_placeholders[ph_name]
764 except KeyError:
765 is_an_injectable = False
766 except:
767 _log.exception('injectable placeholder handling error: %s', original_placeholder_def)
768 if self.debug:
769 return self._escape(self.invalid_placeholder_template % original_placeholder_def)
770 return None
771 if is_an_injectable:
772 if val is None:
773 if self.debug:
774 return self._escape('injectable placeholder [%s]: no value available' % ph_name)
775 return placeholder
776 try:
777 pos_first_char, pos_last_char = self.__parse_region_definition(region_str)
778 except ValueError:
779 if self.debug:
780 return self._escape(self.invalid_placeholder_template % original_placeholder_def)
781 return None
782 if pos_last_char is None:
783 return self.__make_compatible_with_encoding(val)
784
785 if len(val) > (pos_last_char - pos_first_char):
786
787 if self.__ellipsis is not None:
788 return self.__make_compatible_with_encoding(val[pos_first_char:(pos_last_char-len(self.__ellipsis))] + self.__ellipsis)
789 return self.__make_compatible_with_encoding(val[pos_first_char:pos_last_char])
790
791
792 if len(placeholder.split('::', 2)) < 3:
793 _log.error('invalid placeholder structure: %s', original_placeholder_def)
794 if self.debug:
795 return self._escape(self.invalid_placeholder_template % original_placeholder_def)
796 return None
797
798 ph_name, data_and_lng = placeholder.split('::', 1)
799 options, region_str = data_and_lng.rsplit('::', 1)
800 _log.debug('placeholder parts: name=[%s]; region_def=[%s]; options=>>>%s<<<', ph_name, region_str, options)
801 try:
802 pos_first_char, pos_last_char = self.__parse_region_definition(region_str)
803 except ValueError:
804 if self.debug:
805 return self._escape(self.invalid_placeholder_template % original_placeholder_def)
806 return None
807
808 handler = getattr(self, '_get_variant_%s' % ph_name, None)
809 if handler is None:
810 _log.warning('no handler <_get_variant_%s> for placeholder %s', ph_name, original_placeholder_def)
811 if self.debug:
812 return self._escape(self.invalid_placeholder_template % original_placeholder_def)
813 return None
814
815 try:
816 val = handler(data = options)
817 if pos_last_char is None:
818 return self.__make_compatible_with_encoding(val)
819
820 if len(val) > (pos_last_char - pos_first_char):
821
822 if self.__ellipsis is not None:
823 return self.__make_compatible_with_encoding(val[pos_first_char:(pos_last_char-len(self.__ellipsis))] + self.__ellipsis)
824 return self.__make_compatible_with_encoding(val[pos_first_char:pos_last_char])
825 except:
826 _log.exception('placeholder handling error: %s', original_placeholder_def)
827 if self.debug:
828 return self._escape(self.invalid_placeholder_template % original_placeholder_def)
829 return None
830
831 _log.error('something went wrong, should never get here')
832 return None
833
834
835
836
838 options = data.split('//')
839 name = options[0]
840 val = options[1]
841 if name == 'ellipsis':
842 self.ellipsis = val
843 elif name == 'argumentsdivider':
844 self.arguments_divider = val
845 elif name == 'encoding':
846 self.data_encoding = val
847 if len(options) > 2:
848 return options[2] % {'name': name, 'value': val}
849 return ''
850
852 return self._escape (
853 gmTools.coalesce (
854 _cfg.get(option = 'client_version'),
855 '%s' % self.__class__.__name__
856 )
857 )
858
887
903
905
906 select = False
907 include_descriptions = False
908 template = '%s'
909 path_template = None
910 export_path = None
911
912 data_parts = data.split(self.__args_divider)
913
914 if 'select' in data_parts:
915 select = True
916 data_parts.remove('select')
917
918 if 'description' in data_parts:
919 include_descriptions = True
920 data_parts.remove('description')
921
922 template = data_parts[0]
923
924 if len(data_parts) > 1:
925 path_template = data_parts[1]
926
927 if len(data_parts) > 2:
928 export_path = data_parts[2]
929
930
931 if export_path is not None:
932 export_path = os.path.normcase(os.path.expanduser(export_path))
933 gmTools.mkdir(export_path)
934
935
936 if select:
937 docs = gmDocumentWidgets.manage_documents(msg = _('Select the patient documents to reference from the new document.'), single_selection = False)
938 else:
939 docs = self.pat.document_folder.documents
940
941 if docs is None:
942 return ''
943
944 lines = []
945 for doc in docs:
946 lines.append(template % doc.fields_as_dict(date_format = '%Y %b %d', escape_style = self.__esc_style))
947 if include_descriptions:
948 for desc in doc.get_descriptions(max_lng = None):
949 lines.append(self._escape(desc['text'] + '\n'))
950 if path_template is not None:
951 for part_name in doc.save_parts_to_files(export_dir = export_path):
952 path, name = os.path.split(part_name)
953 lines.append(path_template % {'fullpath': part_name, 'name': name})
954
955 return '\n'.join(lines)
956
958
959 encounters = gmEncounterWidgets.select_encounters(single_selection = False)
960 if not encounters:
961 return ''
962
963 template = data
964
965 lines = []
966 for enc in encounters:
967 try:
968 lines.append(template % enc.fields_as_dict(date_format = '%Y %b %d', escape_style = self.__esc_style))
969 except:
970 lines.append('error formatting encounter')
971 _log.exception('problem formatting encounter list')
972 _log.error('template: %s', template)
973 _log.error('encounter: %s', encounter)
974
975 return '\n'.join(lines)
976
978 """Select encounters from list and format SOAP thereof.
979
980 data: soap_cats (' ' -> None -> admin) // date format
981 """
982
983 cats = None
984 date_format = None
985
986 if data is not None:
987 data_parts = data.split(self.__args_divider)
988
989
990 if len(data_parts[0]) > 0:
991 cats = []
992 if ' ' in data_parts[0]:
993 cats.append(None)
994 data_parts[0] = data_parts[0].replace(' ', '')
995 cats.extend(list(data_parts[0]))
996
997
998 if len(data_parts) > 1:
999 if len(data_parts[1]) > 0:
1000 date_format = data_parts[1]
1001
1002 encounters = gmEncounterWidgets.select_encounters(single_selection = False)
1003 if not encounters:
1004 return ''
1005
1006 chunks = []
1007 for enc in encounters:
1008 chunks.append(enc.format_latex (
1009 date_format = date_format,
1010 soap_cats = cats,
1011 soap_order = 'soap_rank, date'
1012 ))
1013
1014 return ''.join(chunks)
1015
1017
1018 cats = list('soapu')
1019 cats.append(None)
1020 template = '%s'
1021 interactive = True
1022 line_length = 9999
1023 time_range = None
1024
1025 if data is not None:
1026 data_parts = data.split(self.__args_divider)
1027
1028
1029 cats = []
1030
1031 for c in list(data_parts[0]):
1032 if c == ' ':
1033 c = None
1034 cats.append(c)
1035
1036 if cats == '':
1037 cats = list('soapu').append(None)
1038
1039
1040 if len(data_parts) > 1:
1041 template = data_parts[1]
1042
1043
1044 if len(data_parts) > 2:
1045 try:
1046 line_length = int(data_parts[2])
1047 except:
1048 line_length = 9999
1049
1050
1051 if len(data_parts) > 3:
1052 try:
1053 time_range = 7 * int(data_parts[3])
1054 except:
1055
1056
1057 time_range = data_parts[3]
1058
1059
1060 narr = self.pat.emr.get_as_journal(soap_cats = cats, time_range = time_range)
1061
1062 if len(narr) == 0:
1063 return ''
1064
1065 keys = narr[0].keys()
1066 lines = []
1067 line_dict = {}
1068 for n in narr:
1069 for key in keys:
1070 if isinstance(n[key], str):
1071 line_dict[key] = self._escape(text = n[key])
1072 continue
1073 line_dict[key] = n[key]
1074 try:
1075 lines.append((template % line_dict)[:line_length])
1076 except KeyError:
1077 return 'invalid key in template [%s], valid keys: %s]' % (template, str(keys))
1078
1079 return '\n'.join(lines)
1080
1082 return self.__get_variant_soap_by_issue_or_episode(data = data, mode = 'issue')
1083
1085 return self.__get_variant_soap_by_issue_or_episode(data = data, mode = 'episode')
1086
1088
1089
1090 cats = list('soapu')
1091 cats.append(None)
1092
1093 date_format = None
1094 template = '%s'
1095
1096 if data is not None:
1097 data_parts = data.split(self.__args_divider)
1098
1099
1100 if len(data_parts[0]) > 0:
1101 cats = []
1102 if ' ' in data_parts[0]:
1103 cats.append(None)
1104 cats.extend(list(data_parts[0].replace(' ', '')))
1105
1106
1107 if len(data_parts) > 1:
1108 if len(data_parts[1]) > 0:
1109 date_format = data_parts[1]
1110
1111
1112 if len(data_parts) > 2:
1113 if len(data_parts[2]) > 0:
1114 template = data_parts[2]
1115
1116 if mode == 'issue':
1117 narr = gmNarrativeWorkflows.select_narrative_by_issue(soap_cats = cats)
1118 else:
1119 narr = gmNarrativeWorkflows.select_narrative_by_episode(soap_cats = cats)
1120
1121 if narr is None:
1122 return ''
1123
1124 if len(narr) == 0:
1125 return ''
1126
1127 try:
1128 narr = [ template % n.fields_as_dict(date_format = date_format, escape_style = self.__esc_style) for n in narr ]
1129 except KeyError:
1130 return 'invalid key in template [%s], valid keys: %s]' % (template, str(narr[0].keys()))
1131
1132 return '\n'.join(narr)
1133
1135 return self._get_variant_soap(data = data)
1136
1138 return self._get_variant_soap(data = 's')
1139
1141 return self._get_variant_soap(data = 'o')
1142
1144 return self._get_variant_soap(data = 'a')
1145
1147 return self._get_variant_soap(data = 'p')
1148
1150 return self._get_variant_soap(data = 'u')
1151
1153 return self._get_variant_soap(data = ' ')
1154
1156
1157
1158 cats = list('soapu')
1159 cats.append(None)
1160 template = '%(narrative)s'
1161
1162 if data is not None:
1163 data_parts = data.split(self.__args_divider)
1164
1165
1166 cats = []
1167
1168 for cat in list(data_parts[0]):
1169 if cat == ' ':
1170 cat = None
1171 cats.append(cat)
1172
1173 if cats == '':
1174 cats = list('soapu')
1175 cats.append(None)
1176
1177
1178 if len(data_parts) > 1:
1179 template = data_parts[1]
1180
1181
1182 narr = gmNarrativeWorkflows.select_narrative(soap_cats = cats)
1183
1184 if narr is None:
1185 return ''
1186
1187 if len(narr) == 0:
1188 return ''
1189
1190
1191
1192
1193 if '%s' in template:
1194 narr = [ self._escape(n['narrative']) for n in narr ]
1195 else:
1196 narr = [ n.fields_as_dict(escape_style = self.__esc_style) for n in narr ]
1197
1198 try:
1199 narr = [ template % n for n in narr ]
1200 except KeyError:
1201 return 'invalid key in template [%s], valid keys: %s]' % (template, str(narr[0].keys()))
1202 except TypeError:
1203 return 'cannot mix "%%s" and "%%(field)s" in template [%s]' % template
1204
1205 return '\n'.join(narr)
1206
1207
1209 return self._get_variant_name(data = '%(title)s')
1210
1212 return self._get_variant_name(data = '%(firstnames)s')
1213
1215 return self._get_variant_name(data = '%(lastnames)s')
1216
1218 if data is None:
1219 return [_('template is missing')]
1220
1221 name = self.pat.get_active_name()
1222
1223 parts = {
1224 'title': self._escape(gmTools.coalesce(name['title'], '')),
1225 'firstnames': self._escape(name['firstnames']),
1226 'lastnames': self._escape(name['lastnames']),
1227 'preferred': self._escape(gmTools.coalesce (
1228 initial = name['preferred'],
1229 instead = ' ',
1230 template_initial = ' "%s" '
1231 ))
1232 }
1233
1234 return data % parts
1235
1236
1239
1240
1241
1243
1244 values = data.split('//', 2)
1245
1246 if len(values) == 2:
1247 male_value, female_value = values
1248 other_value = '<unkown gender>'
1249 elif len(values) == 3:
1250 male_value, female_value, other_value = values
1251 else:
1252 return _('invalid gender mapping layout: [%s]') % data
1253
1254 if self.pat['gender'] == 'm':
1255 return self._escape(male_value)
1256
1257 if self.pat['gender'] == 'f':
1258 return self._escape(female_value)
1259
1260 return self._escape(other_value)
1261
1262
1263
1265
1266 template = '%s'
1267 msg = _('Select the address you want to use !')
1268 cache_id = ''
1269 options = data.split('//', 4)
1270 if len(options) > 0:
1271 template = options[0]
1272 if template.strip() == '':
1273 template = '%s'
1274 if len(options) > 1:
1275 msg = options[1]
1276 if len(options) > 2:
1277 cache_id = options[2]
1278
1279 cache_key = 'generic_address::' + cache_id
1280 try:
1281 adr2use = self.__cache[cache_key]
1282 _log.debug('cache hit (%s): [%s]', cache_key, adr2use)
1283 except KeyError:
1284 adr2use = None
1285
1286 if adr2use is None:
1287 dlg = gmAddressWidgets.cAddressSelectionDlg(None, -1)
1288 dlg.message = msg
1289 choice = dlg.ShowModal()
1290 adr2use = dlg.address
1291 dlg.Destroy()
1292 if choice == wx.ID_CANCEL:
1293 return ''
1294 self.__cache[cache_key] = adr2use
1295
1296 return template % self._escape(adr2use[part])
1297
1299 return self.__get_variant_gen_adr_part(data = data, part = 'street')
1300
1302 return self.__get_variant_gen_adr_part(data = data, part = 'number')
1303
1305 return self.__get_variant_gen_adr_part(data = data, part = 'subunit')
1306
1308 return self.__get_variant_gen_adr_part(data = data, part = 'urb')
1309
1311 return self.__get_variant_gen_adr_part(data = data, part = 'suburb')
1312
1314 return self.__get_variant_gen_adr_part(data = data, part = 'postcode')
1315
1317 return self.__get_variant_gen_adr_part(data = data, part = 'l10n_region')
1318
1320 return self.__get_variant_gen_adr_part(data = data, part = 'l10n_country')
1321
1323
1324 template = '%s'
1325 cache_id = ''
1326 options = data.split('//', 3)
1327 if len(options) > 0:
1328 template = options[0]
1329 if template.strip() == '':
1330 template = '%s'
1331 if len(options) > 1:
1332 cache_id = options[1]
1333
1334 cache_key = 'receiver::' + cache_id
1335 try:
1336 name, adr = self.__cache[cache_key]
1337 _log.debug('cache hit (%s): [%s:%s]', cache_key, name, adr)
1338 except KeyError:
1339 name = None
1340 adr = None
1341
1342 if name is None:
1343 from Gnumed.wxpython import gmFormWidgets
1344 dlg = gmFormWidgets.cReceiverSelectionDlg(None, -1)
1345 dlg.patient = self.pat
1346 choice = dlg.ShowModal()
1347 name = dlg.name
1348 adr = dlg.address
1349 dlg.Destroy()
1350 if choice == wx.ID_CANCEL:
1351 return ''
1352 self.__cache[cache_key] = (name, adr)
1353
1354 if part == 'name':
1355 return template % self._escape(name)
1356
1357 return template % self._escape(gmTools.coalesce(adr[part], ''))
1358
1360 return self.__get_variant_receiver_part(data = data, part = 'name')
1361
1363 return self.__get_variant_receiver_part(data = data, part = 'street')
1364
1366 return self.__get_variant_receiver_part(data = data, part = 'number')
1367
1369 return self.__get_variant_receiver_part(data = data, part = 'subunit')
1370
1372 return self.__get_variant_receiver_part(data = data, part = 'urb')
1373
1375 return self.__get_variant_receiver_part(data = data, part = 'suburb')
1376
1378 return self.__get_variant_receiver_part(data = data, part = 'postcode')
1379
1381 return self.__get_variant_receiver_part(data = data, part = 'l10n_region')
1382
1384 return self.__get_variant_receiver_part(data = data, part = 'l10n_country')
1385
1387
1388 data_parts = data.split(self.__args_divider)
1389
1390
1391 adr_type = data_parts[0].strip()
1392 orig_type = adr_type
1393 if adr_type != '':
1394 adrs = self.pat.get_addresses(address_type = adr_type)
1395 if len(adrs) == 0:
1396 _log.warning('no address for type [%s]', adr_type)
1397 adr_type = ''
1398 if adr_type == '':
1399 _log.debug('asking user for address type')
1400 adr = gmPersonContactWidgets.select_address(missing = orig_type, person = self.pat)
1401 if adr is None:
1402 if self.debug:
1403 return _('no address type replacement selected')
1404 return ''
1405 adr_type = adr['address_type']
1406 adr = self.pat.get_addresses(address_type = adr_type)[0]
1407
1408
1409 template = _('%(street)s %(number)s, %(postcode)s %(urb)s, %(l10n_region)s, %(l10n_country)s')
1410 if len(data_parts) > 1:
1411 if data_parts[1].strip() != '':
1412 template = data_parts[1]
1413
1414 try:
1415 return template % adr.fields_as_dict(escape_style = self.__esc_style)
1416 except Exception:
1417 _log.exception('error formatting address')
1418 _log.error('template: %s', template)
1419
1420 return None
1421
1423 requested_type = data.strip()
1424 cache_key = 'adr-type-%s' % requested_type
1425 try:
1426 type2use = self.__cache[cache_key]
1427 _log.debug('cache hit (%s): [%s] -> [%s]', cache_key, requested_type, type2use)
1428 except KeyError:
1429 type2use = requested_type
1430 if type2use != '':
1431 adrs = self.pat.get_addresses(address_type = type2use)
1432 if len(adrs) == 0:
1433 _log.warning('no address of type [%s] for <%s> field extraction', requested_type, part)
1434 type2use = ''
1435 if type2use == '':
1436 _log.debug('asking user for replacement address type')
1437 adr = gmPersonContactWidgets.select_address(missing = requested_type, person = self.pat)
1438 if adr is None:
1439 _log.debug('no replacement selected')
1440 if self.debug:
1441 return self._escape(_('no address type replacement selected'))
1442 return ''
1443 type2use = adr['address_type']
1444 self.__cache[cache_key] = type2use
1445 _log.debug('caching (%s): [%s] -> [%s]', cache_key, requested_type, type2use)
1446
1447 return self._escape(self.pat.get_addresses(address_type = type2use)[0][part])
1448
1450 return self.__get_variant_adr_part(data = data, part = 'street')
1451
1453 return self.__get_variant_adr_part(data = data, part = 'number')
1454
1456 return self.__get_variant_adr_part(data = data, part = 'subunit')
1457
1459 return self.__get_variant_adr_part(data = data, part = 'urb')
1460
1462 return self.__get_variant_adr_part(data = data, part = 'suburb')
1463
1464 - def _get_variant_adr_postcode(self, data='?'):
1465 return self.__get_variant_adr_part(data = data, part = 'postcode')
1466
1468 return self.__get_variant_adr_part(data = data, part = 'l10n_region')
1469
1471 return self.__get_variant_adr_part(data = data, part = 'l10n_country')
1472
1474 comm_type = None
1475 template = '%(url)s'
1476 if data is not None:
1477 data_parts = data.split(self.__args_divider)
1478 if len(data_parts) > 0:
1479 comm_type = data_parts[0]
1480 if len(data_parts) > 1:
1481 template = data_parts[1]
1482
1483 comms = self.pat.get_comm_channels(comm_medium = comm_type)
1484 if len(comms) == 0:
1485 if self.debug:
1486 return template + ': ' + self._escape(_('no URL for comm channel [%s]') % data)
1487 return ''
1488
1489 return template % comms[0].fields_as_dict(escape_style = self.__esc_style)
1490
1492
1493 template = '%s'
1494 target_mime = None
1495 target_ext = None
1496 if data is not None:
1497 parts = data.split(self.__args_divider)
1498 template = parts[0]
1499 if len(parts) > 1:
1500 target_mime = parts[1].strip()
1501 if len(parts) > 2:
1502 target_ext = parts[2].strip()
1503 if target_ext is None:
1504 if target_mime is not None:
1505 target_ext = gmMimeLib.guess_ext_by_mimetype(mimetype = target_mime)
1506
1507 cache_key = 'patient_photo_path::%s::%s' % (target_mime, target_ext)
1508 try:
1509 fname = self.__cache[cache_key]
1510 _log.debug('cache hit on [%s]: %s', cache_key, fname)
1511 except KeyError:
1512 mugshot = self.pat.document_folder.latest_mugshot
1513 if mugshot is None:
1514 if self.debug:
1515 return self._escape(_('no mugshot available'))
1516 return ''
1517 fname = mugshot.save_to_file (
1518 target_mime = target_mime,
1519 target_extension = target_ext,
1520 ignore_conversion_problems = True
1521 )
1522 if fname is None:
1523 if self.debug:
1524 return self._escape(_('cannot export or convert latest mugshot'))
1525 return ''
1526 self.__cache[cache_key] = fname
1527
1528 return template % fname
1529
1530
1532 options = data.split(self.__args_divider)
1533 template = options[0].strip()
1534 if template == '':
1535 template = '%s'
1536
1537 return template % self.pat.export_as_vcard()
1538
1539
1541 options = data.split(self.__args_divider)
1542 template = options[0].strip()
1543 if template == '':
1544 template = '%s'
1545
1546 return template % self.pat.export_as_gdt()
1547
1548
1565
1566
1567
1568
1569
1570
1572 options = data.split(self.__args_divider)
1573
1574 if 'select' in options:
1575 options.remove('select')
1576 branch = 'select branch'
1577 else:
1578 branch = gmPraxis.cPraxisBranch(aPK_obj = gmPraxis.gmCurrentPraxisBranch()['pk_praxis_branch'])
1579
1580 template = '%s'
1581 if len(options) > 0:
1582 template = options[0]
1583 if template.strip() == '':
1584 template = '%s'
1585
1586 return template % branch.fields_as_dict(escape_style = self.__esc_style)
1587
1588
1590
1591 cache_key = 'current_branch_vcf_path'
1592 try:
1593 vcf_name = self.__cache[cache_key]
1594 _log.debug('cache hit (%s): [%s]', cache_key, vcf_name)
1595 except KeyError:
1596 vcf_name = gmPraxis.gmCurrentPraxisBranch().vcf
1597 self.__cache[cache_key] = vcf_name
1598
1599 template = '%s'
1600 if data.strip() != '':
1601 template = data
1602
1603 return template % vcf_name
1604
1605
1607
1608 options = data.split(self.__args_divider)
1609
1610
1611 template = _('%(street)s %(number)s, %(postcode)s %(urb)s, %(l10n_region)s, %(l10n_country)s')
1612 if len(options) > 0:
1613 if options[0].strip() != '':
1614 template = options[0]
1615
1616 adr = gmPraxis.gmCurrentPraxisBranch().address
1617 if adr is None:
1618 if self.debug:
1619 return _('no address recorded')
1620 return ''
1621 try:
1622 return template % adr.fields_as_dict(escape_style = self.__esc_style)
1623 except Exception:
1624 _log.exception('error formatting address')
1625 _log.error('template: %s', template)
1626
1627 return None
1628
1629
1631 options = data.split(self.__args_divider)
1632 comm_type = options[0]
1633 template = '%(url)s'
1634 if len(options) > 1:
1635 template = options[1]
1636
1637 comms = gmPraxis.gmCurrentPraxisBranch().get_comm_channels(comm_medium = comm_type)
1638 if len(comms) == 0:
1639 if self.debug:
1640 return template + ': ' + self._escape(_('no URL for comm channel [%s]') % data)
1641 return ''
1642
1643 return template % comms[0].fields_as_dict(escape_style = self.__esc_style)
1644
1645
1647 options = data.split(self.__args_divider)
1648 id_type = options[0].strip()
1649 if id_type == '':
1650 return self._escape('praxis external ID: type is missing')
1651
1652 if len(options) > 1:
1653 issuer = options[1].strip()
1654 if issuer == '':
1655 issue = None
1656 else:
1657 issuer = None
1658
1659 if len(options) > 2:
1660 template = options[2]
1661 else:
1662 template = '%(name)s: %(value)s (%(issuer)s)'
1663
1664 ids = gmPraxis.gmCurrentPraxisBranch().get_external_ids(id_type = id_type, issuer = issuer)
1665 if len(ids) == 0:
1666 if self.debug:
1667 return template + ': ' + self._escape(_('no external ID [%s] by [%s]') % (id_type, issuer))
1668 return ''
1669
1670 return template % self._escape_dict(the_dict = ids[0], none_string = '')
1671
1672
1673
1674
1676 prov = gmStaff.gmCurrentProvider()
1677
1678 tmp = '%s%s. %s' % (
1679 gmTools.coalesce(prov['title'], '', '%s '),
1680 prov['firstnames'][:1],
1681 prov['lastnames']
1682 )
1683 return self._escape(tmp)
1684
1685
1687 if data is None:
1688 data = '%(title)s'
1689 elif data.strip() == '':
1690 data = '%(title)s'
1691 return self._get_variant_name(data = data)
1692
1694 if data is None:
1695 data = '%(firstnames)s'
1696 elif data.strip() == '':
1697 data = '%(firstnames)s'
1698 return self._get_variant_name(data = data)
1699
1701 if data is None:
1702 data = '%(lastnames)s'
1703 elif data.strip() == '':
1704 data = '%(lastnames)s'
1705 return self._get_variant_name(data = data)
1706
1720
1721
1723 data_parts = data.split(self.__args_divider)
1724 if len(data_parts) < 2:
1725 return self._escape('current provider external ID: template is missing')
1726
1727 id_type = data_parts[0].strip()
1728 if id_type == '':
1729 return self._escape('current provider external ID: type is missing')
1730
1731 issuer = data_parts[1].strip()
1732 if issuer == '':
1733 return self._escape('current provider external ID: issuer is missing')
1734
1735 prov = gmStaff.gmCurrentProvider()
1736 ids = prov.identity.get_external_ids(id_type = id_type, issuer = issuer)
1737
1738 if len(ids) == 0:
1739 if self.debug:
1740 return self._escape(_('no external ID [%s] by [%s]') % (id_type, issuer))
1741 return ''
1742
1743 return self._escape(ids[0]['value'])
1744
1745
1747 prov = self.pat.primary_provider
1748 if prov is None:
1749 return self._get_variant_current_provider()
1750
1751 title = gmTools.coalesce (
1752 prov['title'],
1753 gmPerson.map_gender2salutation(prov['gender'])
1754 )
1755
1756 tmp = '%s %s. %s' % (
1757 title,
1758 prov['firstnames'][:1],
1759 prov['lastnames']
1760 )
1761 return self._escape(tmp)
1762
1763
1765 data_parts = data.split(self.__args_divider)
1766 if len(data_parts) < 2:
1767 return self._escape('primary in-praxis provider external ID: template is missing')
1768
1769 id_type = data_parts[0].strip()
1770 if id_type == '':
1771 return self._escape('primary in-praxis provider external ID: type is missing')
1772
1773 issuer = data_parts[1].strip()
1774 if issuer == '':
1775 return self._escape('primary in-praxis provider external ID: issuer is missing')
1776
1777 prov = self.pat.primary_provider
1778 if prov is None:
1779 if self.debug:
1780 return self._escape(_('no primary in-praxis provider'))
1781 return ''
1782
1783 ids = prov.identity.get_external_ids(id_type = id_type, issuer = issuer)
1784
1785 if len(ids) == 0:
1786 if self.debug:
1787 return self._escape(_('no external ID [%s] by [%s]') % (id_type, issuer))
1788 return ''
1789
1790 return self._escape(ids[0]['value'])
1791
1792
1794 data_parts = data.split(self.__args_divider)
1795 if len(data_parts) < 2:
1796 return self._escape('patient external ID: template is missing')
1797
1798 id_type = data_parts[0].strip()
1799 if id_type == '':
1800 return self._escape('patient external ID: type is missing')
1801
1802 issuer = data_parts[1].strip()
1803 if issuer == '':
1804 return self._escape('patient external ID: issuer is missing')
1805
1806 ids = self.pat.get_external_ids(id_type = id_type, issuer = issuer)
1807
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 self._escape(ids[0]['value'])
1814
1815
1817 allg_state = self.pat.emr.allergy_state
1818
1819 if allg_state['last_confirmed'] is None:
1820 date_confirmed = ''
1821 else:
1822 date_confirmed = ' (%s)' % gmDateTime.pydt_strftime (
1823 allg_state['last_confirmed'],
1824 format = '%Y %B %d'
1825 )
1826
1827 tmp = '%s%s' % (
1828 allg_state.state_string,
1829 date_confirmed
1830 )
1831 return self._escape(tmp)
1832
1833
1841
1842
1849
1850
1852 return self._get_variant_current_meds_AMTS(data=data, strict=False)
1853
1854
1856
1857
1858 emr = self.pat.emr
1859 from Gnumed.wxpython import gmMedicationWidgets
1860 intakes2export = gmMedicationWidgets.manage_substance_intakes(emr = emr)
1861 if intakes2export is None:
1862 return ''
1863 if len(intakes2export) == 0:
1864 return ''
1865
1866
1867 unique_intakes = {}
1868 for intake in intakes2export:
1869 if intake['pk_drug_product'] is None:
1870 unique_intakes[intake['pk_substance']] = intake
1871 else:
1872 unique_intakes[intake['product']] = intake
1873 del intakes2export
1874 unique_intakes = unique_intakes.values()
1875
1876
1877 self.__create_amts_datamatrix_files(intakes = unique_intakes)
1878
1879
1880 intake_as_latex_rows = []
1881 for intake in unique_intakes:
1882 intake_as_latex_rows.append(intake._get_as_amts_latex(strict = strict))
1883 del unique_intakes
1884
1885
1886
1887 intake_as_latex_rows.extend(emr.allergy_state._get_as_amts_latex(strict = strict))
1888
1889 for allg in emr.get_allergies():
1890 intake_as_latex_rows.append(allg._get_as_amts_latex(strict = strict))
1891
1892
1893 table_rows = intake_as_latex_rows[:15]
1894 if len(intake_as_latex_rows) > 15:
1895 table_rows.append('\\newpage')
1896 table_rows.extend(intake_as_latex_rows[15:30])
1897 if len(intake_as_latex_rows) > 30:
1898 table_rows.append('\\newpage')
1899 table_rows.extend(intake_as_latex_rows[30:45])
1900
1901 if strict:
1902 return '\n'.join(table_rows)
1903
1904
1905 if len(intake_as_latex_rows) > 45:
1906 table_rows.append('\\newpage')
1907 table_rows.extend(intake_as_latex_rows[30:45])
1908
1909 if len(intake_as_latex_rows) > 60:
1910 table_rows.append('\\newpage')
1911 table_rows.extend(intake_as_latex_rows[30:45])
1912
1913 return '\n'.join(table_rows)
1914
1915
1917
1918
1919 for idx in [1,2,3]:
1920 self.set_placeholder(key = 'amts_data_file_%s' % idx, value = './missing-file.txt', known_only = False)
1921 self.set_placeholder(key = 'amts_png_file_%s' % idx, value = './missing-file.png', known_only = False)
1922 self.set_placeholder(key = 'amts_png_file_current_page', value = './missing-file-current-page.png', known_only = False)
1923 self.set_placeholder(key = 'amts_png_file_utf8', value = './missing-file-utf8.png', known_only = False)
1924 self.set_placeholder(key = 'amts_data_file_utf8', value = './missing-file-utf8.txt', known_only = False)
1925
1926
1927 found, dmtx_creator = gmShellAPI.detect_external_binary(binary = 'gm-create_datamatrix')
1928 _log.debug(dmtx_creator)
1929 if not found:
1930 _log.error('gm-create_datamatrix(.bat/.exe) not found')
1931 return
1932
1933 png_dir = gmTools.mk_sandbox_dir()
1934 _log.debug('sandboxing AMTS datamatrix PNGs in: %s', png_dir)
1935
1936 from Gnumed.business import gmForms
1937
1938
1939
1940 amts_data_template_def_file = gmMedication.generate_amts_data_template_definition_file(strict = False)
1941 _log.debug('amts data template definition file: %s', amts_data_template_def_file)
1942 form = gmForms.cTextForm(template_file = amts_data_template_def_file)
1943
1944 amts_sections = '<S>%s</S>' % ''.join ([
1945 i._get_as_amts_data(strict = False) for i in intakes
1946 ])
1947
1948 emr = self.pat.emr
1949 amts_sections += emr.allergy_state._get_as_amts_data(strict = False) % ''.join ([
1950 a._get_as_amts_data(strict = False) for a in emr.get_allergies()
1951 ])
1952 self.set_placeholder(key = 'amts_intakes_as_data_enhanced', value = amts_sections, known_only = False)
1953
1954 self.set_placeholder(key = 'amts_total_pages', value = '1', known_only = False)
1955 success = form.substitute_placeholders(data_source = self)
1956 self.unset_placeholder(key = 'amts_intakes_as_data_enhanced')
1957
1958 self.unset_placeholder(key = 'amts_total_pages')
1959 if not success:
1960 _log.error('cannot substitute into amts data file form template')
1961 return
1962 data_file = form.re_editable_filenames[0]
1963 png_file = os.path.join(png_dir, 'gm4amts-datamatrix-utf8.png')
1964 cmd = '%s %s %s' % (dmtx_creator, data_file, png_file)
1965 success = gmShellAPI.run_command_in_shell(command = cmd, blocking = True)
1966 if not success:
1967 _log.error('error running [%s]' % cmd)
1968 return
1969 self.set_placeholder(key = 'amts_data_file_utf8', value = data_file, known_only = False)
1970 self.set_placeholder(key = 'amts_png_file_utf8', value = png_file, known_only = False)
1971
1972
1973 total_pages = (len(intakes) / 15.0)
1974 if total_pages > int(total_pages):
1975 total_pages += 1
1976 total_pages = int(total_pages)
1977 _log.debug('total pages: %s', total_pages)
1978
1979 png_file_base = os.path.join(png_dir, 'gm4amts-datamatrix-page-')
1980 for this_page in range(1,total_pages+1):
1981 intakes_this_page = intakes[(this_page-1)*15:this_page*15]
1982 amts_data_template_def_file = gmMedication.generate_amts_data_template_definition_file(strict = True)
1983 _log.debug('amts data template definition file: %s', amts_data_template_def_file)
1984 form = gmForms.cTextForm(template_file = amts_data_template_def_file)
1985
1986 amts_sections = '<S>%s</S>' % ''.join ([
1987 i._get_as_amts_data(strict = False) for i in intakes_this_page
1988 ])
1989 if this_page == total_pages:
1990
1991 emr = self.pat.emr
1992 amts_sections += emr.allergy_state._get_as_amts_data(strict = False) % ''.join ([
1993 a._get_as_amts_data(strict = False) for a in emr.get_allergies()
1994 ])
1995 self.set_placeholder(key = 'amts_intakes_as_data', value = amts_sections, known_only = False)
1996
1997 if total_pages == 1:
1998 pg_idx = ''
1999 else:
2000 pg_idx = '%s' % this_page
2001 self.set_placeholder(key = 'amts_page_idx', value = pg_idx, known_only = False)
2002 self.set_placeholder(key = 'amts_total_pages', value = '%s' % total_pages, known_only = False)
2003 success = form.substitute_placeholders(data_source = self)
2004 self.unset_placeholder(key = 'amts_intakes_as_data')
2005
2006 self.unset_placeholder(key = 'amts_page_idx')
2007 self.unset_placeholder(key = 'amts_total_pages')
2008 if not success:
2009 _log.error('cannot substitute into amts data file form template')
2010 return
2011
2012 data_file = form.re_editable_filenames[0]
2013 png_file = '%s%s.png' % (png_file_base, this_page)
2014 latin1_data_file = gmTools.recode_file (
2015 source_file = data_file,
2016 source_encoding = 'utf8',
2017 target_encoding = 'latin1',
2018 base_dir = os.path.split(data_file)[0]
2019 )
2020 cmd = '%s %s %s' % (dmtx_creator, latin1_data_file, png_file)
2021 success = gmShellAPI.run_command_in_shell(command = cmd, blocking = True)
2022 if not success:
2023 _log.error('error running [%s]' % cmd)
2024 return
2025
2026
2027 self.set_placeholder(key = 'amts_data_file_%s' % this_page, value = latin1_data_file, known_only = False)
2028 self.set_placeholder(key = 'amts_png_file_%s' % this_page, value = png_file, known_only = False)
2029
2030 self.set_placeholder(key = 'amts_png_file_current_page', value = png_file_base + '\\thepage', known_only = False)
2031
2032
2034 if data is None:
2035 return self._escape(_('current_meds_for_rx: template is missing'))
2036
2037 emr = self.pat.emr
2038 from Gnumed.wxpython import gmMedicationWidgets
2039 current_meds = gmMedicationWidgets.manage_substance_intakes(emr = emr)
2040 if current_meds is None:
2041 return ''
2042
2043 intakes2show = {}
2044 for intake in current_meds:
2045 fields_dict = intake.fields_as_dict(date_format = '%Y %b %d', escape_style = self.__esc_style)
2046 fields_dict['medically_formatted_start'] = self._escape(intake.medically_formatted_start)
2047 if intake['pk_drug_product'] is None:
2048 fields_dict['product'] = self._escape(_('generic %s') % fields_dict['substance'])
2049 fields_dict['contains'] = self._escape('%s %s%s' % (fields_dict['substance'], fields_dict['amount'], fields_dict['unit']))
2050 intakes2show[fields_dict['product']] = fields_dict
2051 else:
2052 comps = [ c.split('::') for c in intake.containing_drug['components'] ]
2053 fields_dict['contains'] = self._escape('; '.join([ '%s %s%s' % (c[0], c[1], c[2]) for c in comps ]))
2054 intakes2show[intake['product']] = fields_dict
2055
2056 intakes2dispense = {}
2057 for product, intake in intakes2show.items():
2058 msg = _('Dispense how much/many of "%(product)s (%(contains)s)" ?') % intake
2059 amount2dispense = wx.GetTextFromUser(msg, _('Amount to dispense ?'))
2060 if amount2dispense == '':
2061 continue
2062 intake['amount2dispense'] = amount2dispense
2063 intakes2dispense[product] = intake
2064
2065 return '\n'.join([ data % intake for intake in intakes2dispense.values() ])
2066
2067
2082
2083
2117
2124
2131
2137
2169
2175
2177 options = data.split(self.__args_divider)
2178 template = options[0]
2179 if len(options) > 1:
2180 date_format = options[1]
2181 else:
2182 date_format = '%Y %b %d'
2183
2184 vaccs = self.pat.emr.get_vaccinations(order_by = 'date_given DESC, vaccine')
2185
2186 return '\n'.join([ template % v.fields_as_dict(date_format = date_format, escape_style = self.__esc_style) for v in vaccs ])
2187
2189
2190 if data is None:
2191 if self.debug:
2192 _log.error('PHX: missing placeholder arguments')
2193 return self._escape(_('PHX: Invalid placeholder options.'))
2194 return ''
2195
2196 _log.debug('arguments: %s', data)
2197
2198 data_parts = data.split(self.__args_divider)
2199 template = '%s'
2200 separator = '\n'
2201 date_format = '%Y %b %d'
2202 try:
2203 template = data_parts[0]
2204 separator = data_parts[1]
2205 date_format = data_parts[2]
2206 except IndexError:
2207 pass
2208
2209 phxs = gmEMRStructWidgets.select_health_issues(emr = self.pat.emr)
2210 if phxs is None:
2211 if self.debug:
2212 return self._escape(_('no PHX for this patient (available or selected)'))
2213 return ''
2214
2215 return separator.join ([
2216 template % phx.fields_as_dict (
2217 date_format = date_format,
2218 escape_style = self.__esc_style,
2219 bool_strings = (self._escape(_('yes')), self._escape(_('no')))
2220 ) for phx in phxs
2221 ])
2222
2223
2230
2231
2233
2234 if data is None:
2235 return self._escape(_('template is missing'))
2236 template = data
2237 dxs = self.pat.emr.candidate_diagnoses
2238 if len(dxs) == 0:
2239 _log.debug('no diagnoses available')
2240 return ''
2241 selected = gmListWidgets.get_choices_from_list (
2242 msg = _('Select the relevant diagnoses:'),
2243 caption = _('Diagnosis selection'),
2244 columns = [ _('Diagnosis'), _('Marked confidential'), _('Certainty'), _('Source') ],
2245 choices = [[
2246 dx['diagnosis'],
2247 gmTools.bool2subst(dx['explicitely_confidential'], _('yes'), _('no'), _('unknown')),
2248 gmTools.coalesce(dx['diagnostic_certainty_classification'], ''),
2249 dx['source']
2250 ] for dx in dxs
2251 ],
2252 data = dxs,
2253 single_selection = False,
2254 can_return_empty = True
2255 )
2256 if selected is None:
2257 _log.debug('user did not select any diagnoses')
2258 return ''
2259 if len(selected) == 0:
2260 _log.debug('user did not select any diagnoses')
2261 return ''
2262
2263 return '\n'.join(template % self._escape_dict(dx, none_string = '?', bool_strings = [_('yes'), _('no')]) for dx in selected)
2264
2265
2268
2269
2272
2273
2275 return self._escape(urllib.parse.quote(data.encode('utf8')))
2276
2277
2278 - def _get_variant_text_snippet(self, data=None):
2279 data_parts = data.split(self.__args_divider)
2280 keyword = data_parts[0]
2281 template = '%s'
2282 if len(data_parts) > 1:
2283 template = data_parts[1]
2284
2285 expansion = gmKeywordExpansionWidgets.expand_keyword(keyword = keyword, show_list_if_needed = True)
2286
2287 if expansion is None:
2288 if self.debug:
2289 return self._escape(_('no textual expansion found for keyword <%s>') % keyword)
2290 return ''
2291
2292
2293 return template % expansion
2294
2295
2351
2352
2354 if data is None:
2355 return None
2356
2357
2358
2359 return data
2360
2361
2363 if data is None:
2364 return None
2365
2366 parts = data.split(self.__args_divider)
2367 if len(parts) < 3:
2368 return 'IF_NOT_EMPTY lacks <instead> definition'
2369 txt = parts[0]
2370 template = parts[1]
2371 instead = parts[2]
2372
2373 if txt.strip() == '':
2374 return instead
2375 if '%s' in template:
2376 return template % txt
2377 return template
2378
2379
2380 - def _get_variant_free_text(self, data=None):
2381
2382 if data is None:
2383 msg = _('generic text')
2384 else:
2385 msg = data
2386
2387 cache_key = 'free_text::%s' % msg
2388 try:
2389 text = self.__cache[cache_key]
2390 except KeyError:
2391 dlg = gmGuiHelpers.cMultilineTextEntryDlg (
2392 None,
2393 -1,
2394 title = _('Replacing <free_text> placeholder'),
2395 msg = _('Below you can enter free text.\n\n [%s]') % msg
2396 )
2397 dlg.enable_user_formatting = True
2398 decision = dlg.ShowModal()
2399 text = dlg.value.strip()
2400 is_user_formatted = dlg.is_user_formatted
2401 dlg.Destroy()
2402
2403 if decision != wx.ID_SAVE:
2404 if self.debug:
2405 text = _('Text input cancelled by user.')
2406 else:
2407 text = ''
2408
2409 if not is_user_formatted:
2410 text = self._escape(text)
2411
2412 self.__cache[cache_key] = text
2413
2414 return text
2415
2416
2437
2438
2459
2460
2462 try:
2463 bill = self.__cache['bill']
2464 except KeyError:
2465 from Gnumed.wxpython import gmBillingWidgets
2466 bill = gmBillingWidgets.manage_bills(patient = self.pat)
2467 if bill is None:
2468 if self.debug:
2469 return self._escape(_('no bill selected'))
2470 return ''
2471 self.__cache['bill'] = bill
2472 self.__cache['bill-adr'] = bill.address
2473
2474 try:
2475 bill_adr = self.__cache['bill-adr']
2476 except KeyError:
2477 bill_adr = bill.address
2478 self.__cache['bill-adr'] = bill_adr
2479
2480 if bill_adr is None:
2481 if self.debug:
2482 return self._escape(_('[%s] bill has no address') % part)
2483 return ''
2484
2485 if bill_adr[part] is None:
2486 return self._escape('')
2487
2488 if data is None:
2489 return self._escape(bill_adr[part])
2490
2491 if data == '':
2492 return self._escape(bill_adr[part])
2493
2494 return data % self._escape(bill_adr[part])
2495
2496
2498 return self.__get_variant_bill_adr_part(data = data, part = 'street')
2499
2500
2502 return self.__get_variant_bill_adr_part(data = data, part = 'number')
2503
2504
2506 return self.__get_variant_bill_adr_part(data = data, part = 'subunit')
2507
2509 return self.__get_variant_bill_adr_part(data = data, part = 'urb')
2510
2511
2513 return self.__get_variant_bill_adr_part(data = data, part = 'suburb')
2514
2515
2517 return self.__get_variant_bill_adr_part(data = data, part = 'postcode')
2518
2519
2521 return self.__get_variant_bill_adr_part(data = data, part = 'l10n_region')
2522
2523
2525 return self.__get_variant_bill_adr_part(data = data, part = 'l10n_country')
2526
2527
2528
2529
2531 if self.__esc_func is None:
2532 return text
2533 return self.__esc_func(text)
2534
2535
2536 - def _escape_dict(self, the_dict=None, date_format='%Y %b %d %H:%M', none_string='', bool_strings=None):
2537 if bool_strings is None:
2538 bools = {True: _('true'), False: _('false')}
2539 else:
2540 bools = {True: bool_strings[0], False: bool_strings[1]}
2541 data = {}
2542 for field in the_dict.keys():
2543
2544
2545
2546
2547 val = the_dict[field]
2548 if val is None:
2549 data[field] = none_string
2550 continue
2551 if isinstance(val, bool):
2552 data[field] = bools[val]
2553 continue
2554 if isinstance(val, datetime.datetime):
2555 data[field] = gmDateTime.pydt_strftime(val, format = date_format)
2556 if self.__esc_style in ['latex', 'tex']:
2557 data[field] = gmTools.tex_escape_string(data[field])
2558 elif self.__esc_style in ['xetex', 'xelatex']:
2559 data[field] = gmTools.xetex_escape_string(data[field])
2560 continue
2561 try:
2562 data[field] = str(val, encoding = 'utf8', errors = 'replace')
2563 except TypeError:
2564 try:
2565 data[field] = str(val)
2566 except (UnicodeDecodeError, TypeError):
2567 val = '%s' % str(val)
2568 data[field] = val.decode('utf8', 'replace')
2569 if self.__esc_style in ['latex', 'tex']:
2570 data[field] = gmTools.tex_escape_string(data[field])
2571 elif self.__esc_style in ['xetex', 'xelatex']:
2572 data[field] = gmTools.xetex_escape_string(data[field])
2573 return data
2574
2575
2577
2578 _log.debug('testing for placeholders with pattern: %s', first_pass_placeholder_regex)
2579
2580 data_source = gmPlaceholderHandler()
2581 original_line = ''
2582
2583 while True:
2584
2585 line = wx.GetTextFromUser (
2586 _('Enter some text containing a placeholder:'),
2587 _('Testing placeholders'),
2588 centre = True,
2589 default_value = original_line
2590 )
2591 if line.strip() == '':
2592 break
2593 original_line = line
2594
2595 placeholders_in_line = regex.findall(first_pass_placeholder_regex, line, regex.IGNORECASE)
2596 if len(placeholders_in_line) == 0:
2597 continue
2598 for placeholder in placeholders_in_line:
2599 try:
2600 val = data_source[placeholder]
2601 except:
2602 val = _('error with placeholder [%s]') % placeholder
2603 if val is None:
2604 val = _('error with placeholder [%s]') % placeholder
2605 line = line.replace(placeholder, val)
2606
2607 msg = _(
2608 'Input: %s\n'
2609 '\n'
2610 'Output:\n'
2611 '%s'
2612 ) % (
2613 original_line,
2614 line
2615 )
2616 gmGuiHelpers.gm_show_info (
2617 title = _('Testing placeholders'),
2618 info = msg
2619 )
2620
2621
2623 """Functions a macro can legally use.
2624
2625 An instance of this class is passed to the GNUmed scripting
2626 listener. Hence, all actions a macro can legally take must
2627 be defined in this class. Thus we achieve some screening for
2628 security and also thread safety handling.
2629 """
2630
2631 - def __init__(self, personality = None):
2632 if personality is None:
2633 raise gmExceptions.ConstructorError('must specify personality')
2634 self.__personality = personality
2635 self.__attached = 0
2636 self._get_source_personality = None
2637 self.__user_done = False
2638 self.__user_answer = 'no answer yet'
2639 self.__pat = gmPerson.gmCurrentPatient()
2640
2641 self.__auth_cookie = str(random.random())
2642 self.__pat_lock_cookie = str(random.random())
2643 self.__lock_after_load_cookie = str(random.random())
2644
2645 _log.info('slave mode personality is [%s]', personality)
2646
2647
2648
2649 - def attach(self, personality = None):
2650 if self.__attached:
2651 _log.error('attach with [%s] rejected, already serving a client', personality)
2652 return (0, _('attach rejected, already serving a client'))
2653 if personality != self.__personality:
2654 _log.error('rejecting attach to personality [%s], only servicing [%s]' % (personality, self.__personality))
2655 return (0, _('attach to personality [%s] rejected') % personality)
2656 self.__attached = 1
2657 self.__auth_cookie = str(random.random())
2658 return (1, self.__auth_cookie)
2659
2660 - def detach(self, auth_cookie=None):
2661 if not self.__attached:
2662 return 1
2663 if auth_cookie != self.__auth_cookie:
2664 _log.error('rejecting detach() with cookie [%s]' % auth_cookie)
2665 return 0
2666 self.__attached = 0
2667 return 1
2668
2670 if not self.__attached:
2671 return 1
2672 self.__user_done = False
2673
2674 wx.CallAfter(self._force_detach)
2675 return 1
2676
2678 ver = _cfg.get(option = 'client_version')
2679 return "GNUmed %s, %s $Revision: 1.51 $" % (ver, self.__class__.__name__)
2680
2682 """Shuts down this client instance."""
2683 if not self.__attached:
2684 return 0
2685 if auth_cookie != self.__auth_cookie:
2686 _log.error('non-authenticated shutdown_gnumed()')
2687 return 0
2688 wx.CallAfter(self._shutdown_gnumed, forced)
2689 return 1
2690
2692 """Raise ourselves to the top of the desktop."""
2693 if not self.__attached:
2694 return 0
2695 if auth_cookie != self.__auth_cookie:
2696 _log.error('non-authenticated raise_gnumed()')
2697 return 0
2698 return "cMacroPrimitives.raise_gnumed() not implemented"
2699
2701 if not self.__attached:
2702 return 0
2703 if auth_cookie != self.__auth_cookie:
2704 _log.error('non-authenticated get_loaded_plugins()')
2705 return 0
2706 gb = gmGuiBroker.GuiBroker()
2707 return gb['horstspace.notebook.gui'].keys()
2708
2710 """Raise a notebook plugin within GNUmed."""
2711 if not self.__attached:
2712 return 0
2713 if auth_cookie != self.__auth_cookie:
2714 _log.error('non-authenticated raise_notebook_plugin()')
2715 return 0
2716
2717 wx.CallAfter(gmPlugin.raise_notebook_plugin, a_plugin)
2718 return 1
2719
2721 """Load external patient, perhaps create it.
2722
2723 Callers must use get_user_answer() to get status information.
2724 It is unsafe to proceed without knowing the completion state as
2725 the controlled client may be waiting for user input from a
2726 patient selection list.
2727 """
2728 if not self.__attached:
2729 return (0, _('request rejected, you are not attach()ed'))
2730 if auth_cookie != self.__auth_cookie:
2731 _log.error('non-authenticated load_patient_from_external_source()')
2732 return (0, _('rejected load_patient_from_external_source(), not authenticated'))
2733 if self.__pat.locked:
2734 _log.error('patient is locked, cannot load from external source')
2735 return (0, _('current patient is locked'))
2736 self.__user_done = False
2737 wx.CallAfter(self._load_patient_from_external_source)
2738 self.__lock_after_load_cookie = str(random.random())
2739 return (1, self.__lock_after_load_cookie)
2740
2742 if not self.__attached:
2743 return (0, _('request rejected, you are not attach()ed'))
2744 if auth_cookie != self.__auth_cookie:
2745 _log.error('non-authenticated lock_load_patient()')
2746 return (0, _('rejected lock_load_patient(), not authenticated'))
2747
2748 if lock_after_load_cookie != self.__lock_after_load_cookie:
2749 _log.warning('patient lock-after-load request rejected due to wrong cookie [%s]' % lock_after_load_cookie)
2750 return (0, 'patient lock-after-load request rejected, wrong cookie provided')
2751 self.__pat.locked = True
2752 self.__pat_lock_cookie = str(random.random())
2753 return (1, self.__pat_lock_cookie)
2754
2756 if not self.__attached:
2757 return (0, _('request rejected, you are not attach()ed'))
2758 if auth_cookie != self.__auth_cookie:
2759 _log.error('non-authenticated lock_into_patient()')
2760 return (0, _('rejected lock_into_patient(), not authenticated'))
2761 if self.__pat.locked:
2762 _log.error('patient is already locked')
2763 return (0, _('already locked into a patient'))
2764 searcher = gmPersonSearch.cPatientSearcher_SQL()
2765 if type(search_params) == dict:
2766 idents = searcher.get_identities(search_dict=search_params)
2767 raise Exception("must use dto, not search_dict")
2768 else:
2769 idents = searcher.get_identities(search_term=search_params)
2770 if idents is None:
2771 return (0, _('error searching for patient with [%s]/%s') % (search_term, search_dict))
2772 if len(idents) == 0:
2773 return (0, _('no patient found for [%s]/%s') % (search_term, search_dict))
2774
2775 if len(idents) > 1:
2776 return (0, _('several matching patients found for [%s]/%s') % (search_term, search_dict))
2777 if not gmPatSearchWidgets.set_active_patient(patient = idents[0]):
2778 return (0, _('cannot activate patient [%s] (%s/%s)') % (str(idents[0]), search_term, search_dict))
2779 self.__pat.locked = True
2780 self.__pat_lock_cookie = str(random.random())
2781 return (1, self.__pat_lock_cookie)
2782
2784 if not self.__attached:
2785 return (0, _('request rejected, you are not attach()ed'))
2786 if auth_cookie != self.__auth_cookie:
2787 _log.error('non-authenticated unlock_patient()')
2788 return (0, _('rejected unlock_patient, not authenticated'))
2789
2790 if not self.__pat.locked:
2791 return (1, '')
2792
2793 if unlock_cookie != self.__pat_lock_cookie:
2794 _log.warning('patient unlock request rejected due to wrong cookie [%s]' % unlock_cookie)
2795 return (0, 'patient unlock request rejected, wrong cookie provided')
2796 self.__pat.locked = False
2797 return (1, '')
2798
2800 if not self.__attached:
2801 return 0
2802 if auth_cookie != self.__auth_cookie:
2803 _log.error('non-authenticated select_identity()')
2804 return 0
2805 return "cMacroPrimitives.assume_staff_identity() not implemented"
2806
2808 if not self.__user_done:
2809 return (0, 'still waiting')
2810 self.__user_done = False
2811 return (1, self.__user_answer)
2812
2813
2814
2816 msg = _(
2817 'Someone tries to forcibly break the existing\n'
2818 'controlling connection. This may or may not\n'
2819 'have legitimate reasons.\n\n'
2820 'Do you want to allow breaking the connection ?'
2821 )
2822 can_break_conn = gmGuiHelpers.gm_show_question (
2823 aMessage = msg,
2824 aTitle = _('forced detach attempt')
2825 )
2826 if can_break_conn:
2827 self.__user_answer = 1
2828 else:
2829 self.__user_answer = 0
2830 self.__user_done = True
2831 if can_break_conn:
2832 self.__pat.locked = False
2833 self.__attached = 0
2834 return 1
2835
2837 top_win = wx.GetApp().GetTopWindow()
2838 if forced:
2839 top_win.Destroy()
2840 else:
2841 top_win.Close()
2842
2851
2852
2853
2854 if __name__ == '__main__':
2855
2856 if len(sys.argv) < 2:
2857 sys.exit()
2858
2859 if sys.argv[1] != 'test':
2860 sys.exit()
2861
2862 gmI18N.activate_locale()
2863 gmI18N.install_domain()
2864
2865
2867 handler = gmPlaceholderHandler()
2868 handler.debug = True
2869
2870 for placeholder in ['a', 'b']:
2871 print(handler[placeholder])
2872
2873 pat = gmPersonSearch.ask_for_patient()
2874 if pat is None:
2875 return
2876
2877 gmPatSearchWidgets.set_active_patient(patient = pat)
2878
2879 print('DOB (YYYY-MM-DD):', handler['date_of_birth::%Y-%m-%d'])
2880
2881 app = wx.PyWidgetTester(size = (200, 50))
2882
2883 ph = 'progress_notes::ap'
2884 print('%s: %s' % (ph, handler[ph]))
2885
2887
2888 tests = [
2889
2890 '$<lastname>$',
2891 '$<lastname::::3>$',
2892 '$<name::%(title)s %(firstnames)s%(preferred)s%(lastnames)s>$',
2893
2894
2895 'lastname',
2896 '$<lastname',
2897 '$<lastname::',
2898 '$<lastname::>$',
2899 '$<lastname::abc>$',
2900 '$<lastname::abc::>$',
2901 '$<lastname::abc::3>$',
2902 '$<lastname::abc::xyz>$',
2903 '$<lastname::::>$',
2904 '$<lastname::::xyz>$',
2905
2906 '$<date_of_birth::%Y-%m-%d>$',
2907 '$<date_of_birth::%Y-%m-%d::3>$',
2908 '$<date_of_birth::%Y-%m-%d::>$',
2909
2910
2911 '$<adr_location::home::35>$',
2912 '$<gender_mapper::male//female//other::5>$',
2913 '$<current_meds::==> %(product)s %(preparation)s (%(substance)s) <==\n::50>$',
2914 '$<allergy_list::%(descriptor)s, >$',
2915 '$<current_meds_table::latex//>$'
2916
2917
2918
2919
2920
2921
2922
2923
2924
2925
2926
2927
2928
2929
2930 ]
2931
2932
2933
2934
2935
2936 pat = gmPersonSearch.ask_for_patient()
2937 if pat is None:
2938 return
2939
2940 gmPatSearchWidgets.set_active_patient(patient = pat)
2941
2942 handler = gmPlaceholderHandler()
2943 handler.debug = True
2944
2945 for placeholder in tests:
2946 print(placeholder, "=>", handler[placeholder])
2947 print("--------------")
2948 input()
2949
2950
2951
2952
2953
2954
2955
2956
2957
2959 from Gnumed.pycommon import gmScriptingListener
2960 import xmlrpc.client
2961
2962 listener = gmScriptingListener.cScriptingListener(macro_executor = cMacroPrimitives(personality='unit test'), port=9999)
2963
2964 s = xmlrpc.client.ServerProxy('http://localhost:9999')
2965 print("should fail:", s.attach())
2966 print("should fail:", s.attach('wrong cookie'))
2967 print("should work:", s.version())
2968 print("should fail:", s.raise_gnumed())
2969 print("should fail:", s.raise_notebook_plugin('test plugin'))
2970 print("should fail:", s.lock_into_patient('kirk, james'))
2971 print("should fail:", s.unlock_patient())
2972 status, conn_auth = s.attach('unit test')
2973 print("should work:", status, conn_auth)
2974 print("should work:", s.version())
2975 print("should work:", s.raise_gnumed(conn_auth))
2976 status, pat_auth = s.lock_into_patient(conn_auth, 'kirk, james')
2977 print("should work:", status, pat_auth)
2978 print("should fail:", s.unlock_patient(conn_auth, 'bogus patient unlock cookie'))
2979 print("should work", s.unlock_patient(conn_auth, pat_auth))
2980 data = {'firstname': 'jame', 'lastnames': 'Kirk', 'gender': 'm'}
2981 status, pat_auth = s.lock_into_patient(conn_auth, data)
2982 print("should work:", status, pat_auth)
2983 print("should work", s.unlock_patient(conn_auth, pat_auth))
2984 print(s.detach('bogus detach cookie'))
2985 print(s.detach(conn_auth))
2986 del s
2987
2988 listener.shutdown()
2989
2991
2992 import re as regex
2993
2994 tests = [
2995 ' $<lastname>$ ',
2996 ' $<lastname::::3>$ ',
2997
2998
2999 '$<date_of_birth::%Y-%m-%d>$',
3000 '$<date_of_birth::%Y-%m-%d::3>$',
3001 '$<date_of_birth::%Y-%m-%d::>$',
3002
3003 '$<adr_location::home::35>$',
3004 '$<gender_mapper::male//female//other::5>$',
3005 '$<current_meds::==> %(product)s %(preparation)s (%(substance)s) <==\\n::50>$',
3006 '$<allergy_list::%(descriptor)s, >$',
3007
3008 '\\noindent Patient: $<lastname>$, $<firstname>$',
3009 '$<allergies::%(descriptor)s & %(l10n_type)s & {\\footnotesize %(reaction)s} \tabularnewline \hline >$',
3010 '$<current_meds:: \item[%(substance)s] {\\footnotesize (%(product)s)} %(preparation)s %(amount)s%(unit)s: %(schedule)s >$'
3011 ]
3012
3013 tests = [
3014
3015 'junk $<lastname::::3>$ junk',
3016 'junk $<lastname::abc::3>$ junk',
3017 'junk $<lastname::abc>$ junk',
3018 'junk $<lastname>$ junk',
3019
3020 'junk $<lastname>$ junk $<firstname>$ junk',
3021 'junk $<lastname::abc>$ junk $<fiststname::abc>$ junk',
3022 'junk $<lastname::abc::3>$ junk $<firstname::abc::3>$ junk',
3023 'junk $<lastname::::3>$ junk $<firstname::::3>$ junk'
3024
3025 ]
3026
3027 tests = [
3028
3029
3030
3031
3032
3033 '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',
3034 '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',
3035 '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',
3036
3037 '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',
3038 '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',
3039 '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',
3040 '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',
3041 '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',
3042 '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',
3043 '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',
3044 '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',
3045 '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',
3046 ]
3047
3048 tests = [
3049 'junk $<<<should pass::template::>>>$ junk',
3050 'junk $<<<should pass::template::10>>>$ junk',
3051 'junk $<<<should pass::template::10-20>>>$ junk',
3052 'junk $<<<should pass::template $<<dummy::template 2::10>>$::>>>$ junk',
3053 'junk $<<<should pass::template $<dummy::template 2::10>$::>>>$ junk',
3054
3055 'junk $<<<should pass::template::>>>$ junk $<<<should pass 2::template 2::>>>$ junk',
3056 'junk $<<<should pass::template::>>>$ junk $<<should pass 2::template 2::>>$ junk',
3057 'junk $<<<should pass::template::>>>$ junk $<should pass 2::template 2::>$ junk',
3058
3059 'junk $<<<should fail::template $<<<dummy::template 2::10>>>$::>>>$ junk',
3060
3061 'junk $<<<should fail::template::10->>>$ junk',
3062 'junk $<<<should fail::template::10->>>$ junk',
3063 'junk $<<<should fail::template::10->>>$ junk',
3064 'junk $<<<should fail::template::10->>>$ junk',
3065 'junk $<first_pass::junk $<<<3rd_pass::template::20>>>$ junk::8-10>$ junk'
3066 ]
3067
3068
3069
3070
3071
3072
3073
3074
3075
3076
3077
3078
3079
3080
3081
3082 all_tests = {
3083 first_pass_placeholder_regex: [
3084
3085 ('junk $<first_level::template::>$ junk', ['$<first_level::template::>$']),
3086 ('junk $<first_level::template::10>$ junk', ['$<first_level::template::10>$']),
3087 ('junk $<first_level::template::10-12>$ junk', ['$<first_level::template::10-12>$']),
3088
3089
3090 ('junk $<first_level::$<<insert::insert_template::0>>$::10-12>$ junk', ['$<first_level::$<<insert::insert_template::0>>$::10-12>$']),
3091 ('junk $<first_level::$<<<insert::insert_template::0>>>$::10-12>$ junk', ['$<first_level::$<<<insert::insert_template::0>>>$::10-12>$']),
3092
3093
3094 ('junk $<<second_level::$<insert::insert_template::0>$::10-12>>$ junk', ['$<insert::insert_template::0>$']),
3095 ('junk $<<<third_level::$<insert::insert_template::0>$::10-12>>>$ junk', ['$<insert::insert_template::0>$']),
3096
3097
3098 ('junk $<first_level 1::template 1::>$ junk $<<second_level 2::template 2::>>$ junk', ['$<first_level 1::template 1::>$']),
3099 ('junk $<first_level 1::template 1::>$ junk $<<<third_level 2::template 2::>>>$ junk', ['$<first_level 1::template 1::>$']),
3100
3101
3102 ('junk $<first_level 1::template 1::>$ junk $<first_level 2::template 2::>$ junk', ['$<first_level 1::template 1::>$', '$<first_level 2::template 2::>$']),
3103
3104
3105 ('returns illegal match: junk $<first_level::$<insert::insert_template::0>$::10-12>$ junk', ['$<first_level::$<insert::insert_template::0>$::10-12>$']),
3106 ],
3107 second_pass_placeholder_regex: [
3108
3109 ('junk $<<second_level::template::>>$ junk', ['$<<second_level::template::>>$']),
3110 ('junk $<<second_level::template::10>>$ junk', ['$<<second_level::template::10>>$']),
3111 ('junk $<<second_level::template::10-12>>$ junk', ['$<<second_level::template::10-12>>$']),
3112
3113
3114 ('junk $<<second_level::$<insert::insert_template::0>$::10-12>>$ junk', ['$<<second_level::$<insert::insert_template::0>$::10-12>>$']),
3115 ('junk $<<second_level::$<<<insert::insert_template::0>>>$::10-12>>$ junk', ['$<<second_level::$<<<insert::insert_template::0>>>$::10-12>>$']),
3116
3117
3118 ('junk $<first_level::$<<insert::insert_template::0>>$::10-12>$ junk', ['$<<insert::insert_template::0>>$']),
3119 ('junk $<<<third_level::$<<insert::insert_template::0>>$::10-12>>>$ junk', ['$<<insert::insert_template::0>>$']),
3120
3121
3122 ('junk $<first_level 1::template 1::>$ junk $<<second_level 2::template 2::>>$ junk', ['$<<second_level 2::template 2::>>$']),
3123 ('junk $<<second_level 1::template 1::>>$ junk $<<<third_level 2::template 2::>>>$ junk', ['$<<second_level 1::template 1::>>$']),
3124
3125
3126 ('junk $<<second_level 1::template 1::>>$ junk $<<second_level 2::template 2::>>$ junk', ['$<<second_level 1::template 1::>>$', '$<<second_level 2::template 2::>>$']),
3127
3128
3129 ('returns illegal match: junk $<<second_level::$<<insert::insert_template::0>>$::10-12>>$ junk', ['$<<second_level::$<<insert::insert_template::0>>$::10-12>>$']),
3130
3131 ],
3132 third_pass_placeholder_regex: [
3133
3134 ('junk $<<<third_level::template::>>>$ junk', ['$<<<third_level::template::>>>$']),
3135 ('junk $<<<third_level::template::10>>>$ junk', ['$<<<third_level::template::10>>>$']),
3136 ('junk $<<<third_level::template::10-12>>>$ junk', ['$<<<third_level::template::10-12>>>$']),
3137
3138
3139 ('junk $<<<third_level::$<<insert::insert_template::0>>$::10-12>>>$ junk', ['$<<<third_level::$<<insert::insert_template::0>>$::10-12>>>$']),
3140 ('junk $<<<third_level::$<insert::insert_template::0>$::10-12>>>$ junk', ['$<<<third_level::$<insert::insert_template::0>$::10-12>>>$']),
3141
3142
3143 ('junk $<<second_level::$<<<insert::insert_template::0>>>$::10-12>>$ junk', ['$<<<insert::insert_template::0>>>$']),
3144 ('junk $<first_level::$<<<insert::insert_template::0>>>$::10-12>$ junk', ['$<<<insert::insert_template::0>>>$']),
3145
3146
3147 ('junk $<first_level 1::template 1::>$ junk $<<<third_level 2::template 2::>>>$ junk', ['$<<<third_level 2::template 2::>>>$']),
3148 ('junk $<<second_level 1::template 1::>>$ junk $<<<third_level 2::template 2::>>>$ junk', ['$<<<third_level 2::template 2::>>>$']),
3149
3150
3151 ('returns illegal match: junk $<<<third_level::$<<<insert::insert_template::0>>>$::10-12>>>$ junk', ['$<<<third_level::$<<<insert::insert_template::0>>>$::10-12>>>$']),
3152 ]
3153 }
3154
3155 for pattern in [first_pass_placeholder_regex, second_pass_placeholder_regex, third_pass_placeholder_regex]:
3156 print("")
3157 print("-----------------------------")
3158 print("regex:", pattern)
3159 tests = all_tests[pattern]
3160 for t in tests:
3161 line, expected_results = t
3162 phs = regex.findall(pattern, line, regex.IGNORECASE)
3163 if len(phs) > 0:
3164 if phs == expected_results:
3165 continue
3166
3167 print("")
3168 print("failed")
3169 print("line:", line)
3170
3171 if len(phs) == 0:
3172 print("no match")
3173 continue
3174
3175 if len(phs) > 1:
3176 print("several matches")
3177 for r in expected_results:
3178 print("expected:", r)
3179 for p in phs:
3180 print("found:", p)
3181 continue
3182
3183 print("unexpected match")
3184 print("expected:", expected_results)
3185 print("found: ", phs)
3186
3187
3275
3276
3277
3285
3286
3289
3290
3291
3292 app = wx.App()
3293
3294
3295
3296
3297
3298
3299 test_placeholder()
3300
3301