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 template = u'%(title)s'
1689 elif data.strip() == u'':
1690 data = u'%(title)s'
1691 return self._get_variant_current_provider_name(data = data)
1692
1693
1695 if data is None:
1696 data = u'%(firstnames)s'
1697 elif data.strip() == u'':
1698 data = u'%(firstnames)s'
1699 return self._get_variant_current_provider_name(data = data)
1700
1701
1703 if data is None:
1704 data = u'%(lastnames)s'
1705 elif data.strip() == u'':
1706 data = u'%(lastnames)s'
1707 return self._get_variant_current_provider_name(data = data)
1708
1709
1723
1724
1726 data_parts = data.split(self.__args_divider)
1727 if len(data_parts) < 2:
1728 return self._escape('current provider external ID: template is missing')
1729
1730 id_type = data_parts[0].strip()
1731 if id_type == '':
1732 return self._escape('current provider external ID: type is missing')
1733
1734 issuer = data_parts[1].strip()
1735 if issuer == '':
1736 return self._escape('current provider external ID: issuer is missing')
1737
1738 prov = gmStaff.gmCurrentProvider()
1739 ids = prov.identity.get_external_ids(id_type = id_type, issuer = issuer)
1740
1741 if len(ids) == 0:
1742 if self.debug:
1743 return self._escape(_('no external ID [%s] by [%s]') % (id_type, issuer))
1744 return ''
1745
1746 return self._escape(ids[0]['value'])
1747
1748
1750 prov = self.pat.primary_provider
1751 if prov is None:
1752 return self._get_variant_current_provider()
1753
1754 title = gmTools.coalesce (
1755 prov['title'],
1756 gmPerson.map_gender2salutation(prov['gender'])
1757 )
1758
1759 tmp = '%s %s. %s' % (
1760 title,
1761 prov['firstnames'][:1],
1762 prov['lastnames']
1763 )
1764 return self._escape(tmp)
1765
1766
1768 data_parts = data.split(self.__args_divider)
1769 if len(data_parts) < 2:
1770 return self._escape('primary in-praxis provider external ID: template is missing')
1771
1772 id_type = data_parts[0].strip()
1773 if id_type == '':
1774 return self._escape('primary in-praxis provider external ID: type is missing')
1775
1776 issuer = data_parts[1].strip()
1777 if issuer == '':
1778 return self._escape('primary in-praxis provider external ID: issuer is missing')
1779
1780 prov = self.pat.primary_provider
1781 if prov is None:
1782 if self.debug:
1783 return self._escape(_('no primary in-praxis provider'))
1784 return ''
1785
1786 ids = prov.identity.get_external_ids(id_type = id_type, issuer = issuer)
1787
1788 if len(ids) == 0:
1789 if self.debug:
1790 return self._escape(_('no external ID [%s] by [%s]') % (id_type, issuer))
1791 return ''
1792
1793 return self._escape(ids[0]['value'])
1794
1795
1797 data_parts = data.split(self.__args_divider)
1798 if len(data_parts) < 2:
1799 return self._escape('patient external ID: template is missing')
1800
1801 id_type = data_parts[0].strip()
1802 if id_type == '':
1803 return self._escape('patient external ID: type is missing')
1804
1805 issuer = data_parts[1].strip()
1806 if issuer == '':
1807 return self._escape('patient external ID: issuer is missing')
1808
1809 ids = self.pat.get_external_ids(id_type = id_type, issuer = issuer)
1810
1811 if len(ids) == 0:
1812 if self.debug:
1813 return self._escape(_('no external ID [%s] by [%s]') % (id_type, issuer))
1814 return ''
1815
1816 return self._escape(ids[0]['value'])
1817
1818
1820 allg_state = self.pat.emr.allergy_state
1821
1822 if allg_state['last_confirmed'] is None:
1823 date_confirmed = ''
1824 else:
1825 date_confirmed = ' (%s)' % gmDateTime.pydt_strftime (
1826 allg_state['last_confirmed'],
1827 format = '%Y %B %d'
1828 )
1829
1830 tmp = '%s%s' % (
1831 allg_state.state_string,
1832 date_confirmed
1833 )
1834 return self._escape(tmp)
1835
1836
1844
1845
1852
1853
1855 return self._get_variant_current_meds_AMTS(data=data, strict=False)
1856
1857
1859
1860
1861 emr = self.pat.emr
1862 from Gnumed.wxpython import gmMedicationWidgets
1863 intakes2export = gmMedicationWidgets.manage_substance_intakes(emr = emr)
1864 if intakes2export is None:
1865 return ''
1866 if len(intakes2export) == 0:
1867 return ''
1868
1869
1870 unique_intakes = {}
1871 for intake in intakes2export:
1872 if intake['pk_drug_product'] is None:
1873 unique_intakes[intake['pk_substance']] = intake
1874 else:
1875 unique_intakes[intake['product']] = intake
1876 del intakes2export
1877 unique_intakes = unique_intakes.values()
1878
1879
1880 self.__create_amts_datamatrix_files(intakes = unique_intakes)
1881
1882
1883 intake_as_latex_rows = []
1884 for intake in unique_intakes:
1885 intake_as_latex_rows.append(intake._get_as_amts_latex(strict = strict))
1886 del unique_intakes
1887
1888
1889
1890 intake_as_latex_rows.extend(emr.allergy_state._get_as_amts_latex(strict = strict))
1891
1892 for allg in emr.get_allergies():
1893 intake_as_latex_rows.append(allg._get_as_amts_latex(strict = strict))
1894
1895
1896 table_rows = intake_as_latex_rows[:15]
1897 if len(intake_as_latex_rows) > 15:
1898 table_rows.append('\\newpage')
1899 table_rows.extend(intake_as_latex_rows[15:30])
1900 if len(intake_as_latex_rows) > 30:
1901 table_rows.append('\\newpage')
1902 table_rows.extend(intake_as_latex_rows[30:45])
1903
1904 if strict:
1905 return '\n'.join(table_rows)
1906
1907
1908 if len(intake_as_latex_rows) > 45:
1909 table_rows.append('\\newpage')
1910 table_rows.extend(intake_as_latex_rows[30:45])
1911
1912 if len(intake_as_latex_rows) > 60:
1913 table_rows.append('\\newpage')
1914 table_rows.extend(intake_as_latex_rows[30:45])
1915
1916 return '\n'.join(table_rows)
1917
1918
1920
1921
1922 for idx in [1,2,3]:
1923 self.set_placeholder(key = 'amts_data_file_%s' % idx, value = './missing-file.txt', known_only = False)
1924 self.set_placeholder(key = 'amts_png_file_%s' % idx, value = './missing-file.png', known_only = False)
1925 self.set_placeholder(key = 'amts_png_file_current_page', value = './missing-file-current-page.png', known_only = False)
1926 self.set_placeholder(key = 'amts_png_file_utf8', value = './missing-file-utf8.png', known_only = False)
1927 self.set_placeholder(key = 'amts_data_file_utf8', value = './missing-file-utf8.txt', known_only = False)
1928
1929
1930 found, dmtx_creator = gmShellAPI.detect_external_binary(binary = 'gm-create_datamatrix')
1931 _log.debug(dmtx_creator)
1932 if not found:
1933 _log.error('gm-create_datamatrix(.bat/.exe) not found')
1934 return
1935
1936 png_dir = gmTools.mk_sandbox_dir()
1937 _log.debug('sandboxing AMTS datamatrix PNGs in: %s', png_dir)
1938
1939 from Gnumed.business import gmForms
1940
1941
1942
1943 amts_data_template_def_file = gmMedication.generate_amts_data_template_definition_file(strict = False)
1944 _log.debug('amts data template definition file: %s', amts_data_template_def_file)
1945 form = gmForms.cTextForm(template_file = amts_data_template_def_file)
1946
1947 amts_sections = '<S>%s</S>' % ''.join ([
1948 i._get_as_amts_data(strict = False) for i in intakes
1949 ])
1950
1951 emr = self.pat.emr
1952 amts_sections += emr.allergy_state._get_as_amts_data(strict = False) % ''.join ([
1953 a._get_as_amts_data(strict = False) for a in emr.get_allergies()
1954 ])
1955 self.set_placeholder(key = 'amts_intakes_as_data_enhanced', value = amts_sections, known_only = False)
1956
1957 self.set_placeholder(key = 'amts_total_pages', value = '1', known_only = False)
1958 success = form.substitute_placeholders(data_source = self)
1959 self.unset_placeholder(key = 'amts_intakes_as_data_enhanced')
1960
1961 self.unset_placeholder(key = 'amts_total_pages')
1962 if not success:
1963 _log.error('cannot substitute into amts data file form template')
1964 return
1965 data_file = form.re_editable_filenames[0]
1966 png_file = os.path.join(png_dir, 'gm4amts-datamatrix-utf8.png')
1967 cmd = '%s %s %s' % (dmtx_creator, data_file, png_file)
1968 success = gmShellAPI.run_command_in_shell(command = cmd, blocking = True)
1969 if not success:
1970 _log.error('error running [%s]' % cmd)
1971 return
1972 self.set_placeholder(key = 'amts_data_file_utf8', value = data_file, known_only = False)
1973 self.set_placeholder(key = 'amts_png_file_utf8', value = png_file, known_only = False)
1974
1975
1976 total_pages = (len(intakes) / 15.0)
1977 if total_pages > int(total_pages):
1978 total_pages += 1
1979 total_pages = int(total_pages)
1980 _log.debug('total pages: %s', total_pages)
1981
1982 png_file_base = os.path.join(png_dir, 'gm4amts-datamatrix-page-')
1983 for this_page in range(1,total_pages+1):
1984 intakes_this_page = intakes[(this_page-1)*15:this_page*15]
1985 amts_data_template_def_file = gmMedication.generate_amts_data_template_definition_file(strict = True)
1986 _log.debug('amts data template definition file: %s', amts_data_template_def_file)
1987 form = gmForms.cTextForm(template_file = amts_data_template_def_file)
1988
1989 amts_sections = '<S>%s</S>' % ''.join ([
1990 i._get_as_amts_data(strict = False) for i in intakes_this_page
1991 ])
1992 if this_page == total_pages:
1993
1994 emr = self.pat.emr
1995 amts_sections += emr.allergy_state._get_as_amts_data(strict = False) % ''.join ([
1996 a._get_as_amts_data(strict = False) for a in emr.get_allergies()
1997 ])
1998 self.set_placeholder(key = 'amts_intakes_as_data', value = amts_sections, known_only = False)
1999
2000 if total_pages == 1:
2001 pg_idx = ''
2002 else:
2003 pg_idx = '%s' % this_page
2004 self.set_placeholder(key = 'amts_page_idx', value = pg_idx, known_only = False)
2005 self.set_placeholder(key = 'amts_total_pages', value = '%s' % total_pages, known_only = False)
2006 success = form.substitute_placeholders(data_source = self)
2007 self.unset_placeholder(key = 'amts_intakes_as_data')
2008
2009 self.unset_placeholder(key = 'amts_page_idx')
2010 self.unset_placeholder(key = 'amts_total_pages')
2011 if not success:
2012 _log.error('cannot substitute into amts data file form template')
2013 return
2014
2015 data_file = form.re_editable_filenames[0]
2016 png_file = '%s%s.png' % (png_file_base, this_page)
2017 latin1_data_file = gmTools.recode_file (
2018 source_file = data_file,
2019 source_encoding = 'utf8',
2020 target_encoding = 'latin1',
2021 base_dir = os.path.split(data_file)[0]
2022 )
2023 cmd = '%s %s %s' % (dmtx_creator, latin1_data_file, png_file)
2024 success = gmShellAPI.run_command_in_shell(command = cmd, blocking = True)
2025 if not success:
2026 _log.error('error running [%s]' % cmd)
2027 return
2028
2029
2030 self.set_placeholder(key = 'amts_data_file_%s' % this_page, value = latin1_data_file, known_only = False)
2031 self.set_placeholder(key = 'amts_png_file_%s' % this_page, value = png_file, known_only = False)
2032
2033 self.set_placeholder(key = 'amts_png_file_current_page', value = png_file_base + '\\thepage', known_only = False)
2034
2035
2037 if data is None:
2038 return self._escape(_('current_meds_for_rx: template is missing'))
2039
2040 emr = self.pat.emr
2041 from Gnumed.wxpython import gmMedicationWidgets
2042 current_meds = gmMedicationWidgets.manage_substance_intakes(emr = emr)
2043 if current_meds is None:
2044 return ''
2045
2046 intakes2show = {}
2047 for intake in current_meds:
2048 fields_dict = intake.fields_as_dict(date_format = '%Y %b %d', escape_style = self.__esc_style)
2049 fields_dict['medically_formatted_start'] = self._escape(intake.medically_formatted_start)
2050 if intake['pk_drug_product'] is None:
2051 fields_dict['product'] = self._escape(_('generic %s') % fields_dict['substance'])
2052 fields_dict['contains'] = self._escape('%s %s%s' % (fields_dict['substance'], fields_dict['amount'], fields_dict['unit']))
2053 intakes2show[fields_dict['product']] = fields_dict
2054 else:
2055 comps = [ c.split('::') for c in intake.containing_drug['components'] ]
2056 fields_dict['contains'] = self._escape('; '.join([ '%s %s%s' % (c[0], c[1], c[2]) for c in comps ]))
2057 intakes2show[intake['product']] = fields_dict
2058
2059 intakes2dispense = {}
2060 for product, intake in intakes2show.items():
2061 msg = _('Dispense how much/many of "%(product)s (%(contains)s)" ?') % intake
2062 amount2dispense = wx.GetTextFromUser(msg, _('Amount to dispense ?'))
2063 if amount2dispense == '':
2064 continue
2065 intake['amount2dispense'] = amount2dispense
2066 intakes2dispense[product] = intake
2067
2068 return '\n'.join([ data % intake for intake in intakes2dispense.values() ])
2069
2070
2085
2086
2120
2127
2134
2140
2172
2178
2180 options = data.split(self.__args_divider)
2181 template = options[0]
2182 if len(options) > 1:
2183 date_format = options[1]
2184 else:
2185 date_format = '%Y %b %d'
2186
2187 vaccs = self.pat.emr.get_vaccinations(order_by = 'date_given DESC, vaccine')
2188
2189 return '\n'.join([ template % v.fields_as_dict(date_format = date_format, escape_style = self.__esc_style) for v in vaccs ])
2190
2192
2193 if data is None:
2194 if self.debug:
2195 _log.error('PHX: missing placeholder arguments')
2196 return self._escape(_('PHX: Invalid placeholder options.'))
2197 return ''
2198
2199 _log.debug('arguments: %s', data)
2200
2201 data_parts = data.split(self.__args_divider)
2202 template = '%s'
2203 separator = '\n'
2204 date_format = '%Y %b %d'
2205 try:
2206 template = data_parts[0]
2207 separator = data_parts[1]
2208 date_format = data_parts[2]
2209 except IndexError:
2210 pass
2211
2212 phxs = gmEMRStructWidgets.select_health_issues(emr = self.pat.emr)
2213 if phxs is None:
2214 if self.debug:
2215 return self._escape(_('no PHX for this patient (available or selected)'))
2216 return ''
2217
2218 return separator.join ([
2219 template % phx.fields_as_dict (
2220 date_format = date_format,
2221 escape_style = self.__esc_style,
2222 bool_strings = (self._escape(_('yes')), self._escape(_('no')))
2223 ) for phx in phxs
2224 ])
2225
2226
2233
2234
2236
2237 if data is None:
2238 return self._escape(_('template is missing'))
2239 template = data
2240 dxs = self.pat.emr.candidate_diagnoses
2241 if len(dxs) == 0:
2242 _log.debug('no diagnoses available')
2243 return ''
2244 selected = gmListWidgets.get_choices_from_list (
2245 msg = _('Select the relevant diagnoses:'),
2246 caption = _('Diagnosis selection'),
2247 columns = [ _('Diagnosis'), _('Marked confidential'), _('Certainty'), _('Source') ],
2248 choices = [[
2249 dx['diagnosis'],
2250 gmTools.bool2subst(dx['explicitely_confidential'], _('yes'), _('no'), _('unknown')),
2251 gmTools.coalesce(dx['diagnostic_certainty_classification'], ''),
2252 dx['source']
2253 ] for dx in dxs
2254 ],
2255 data = dxs,
2256 single_selection = False,
2257 can_return_empty = True
2258 )
2259 if selected is None:
2260 _log.debug('user did not select any diagnoses')
2261 return ''
2262 if len(selected) == 0:
2263 _log.debug('user did not select any diagnoses')
2264 return ''
2265
2266 return '\n'.join(template % self._escape_dict(dx, none_string = '?', bool_strings = [_('yes'), _('no')]) for dx in selected)
2267
2268
2271
2272
2275
2276
2278 return self._escape(urllib.parse.quote(data.encode('utf8')))
2279
2280
2281 - def _get_variant_text_snippet(self, data=None):
2282 data_parts = data.split(self.__args_divider)
2283 keyword = data_parts[0]
2284 template = '%s'
2285 if len(data_parts) > 1:
2286 template = data_parts[1]
2287
2288 expansion = gmKeywordExpansionWidgets.expand_keyword(keyword = keyword, show_list_if_needed = True)
2289
2290 if expansion is None:
2291 if self.debug:
2292 return self._escape(_('no textual expansion found for keyword <%s>') % keyword)
2293 return ''
2294
2295
2296 return template % expansion
2297
2298
2354
2355
2357 if data is None:
2358 return None
2359
2360
2361
2362 return data
2363
2364
2366 if data is None:
2367 return None
2368
2369 parts = data.split(self.__args_divider)
2370 if len(parts) < 3:
2371 return 'IF_NOT_EMPTY lacks <instead> definition'
2372 txt = parts[0]
2373 template = parts[1]
2374 instead = parts[2]
2375
2376 if txt.strip() == '':
2377 return instead
2378 if '%s' in template:
2379 return template % txt
2380 return template
2381
2382
2383 - def _get_variant_free_text(self, data=None):
2384
2385 if data is None:
2386 msg = _('generic text')
2387 cache_key = 'free_text::%s' % datetime.datetime.now()
2388 else:
2389 msg = data
2390 cache_key = 'free_text::%s' % msg
2391
2392 try:
2393 return self.__cache[cache_key]
2394 except KeyError:
2395 pass
2396
2397 dlg = gmGuiHelpers.cMultilineTextEntryDlg (
2398 None,
2399 -1,
2400 title = _('Replacing <free_text> placeholder'),
2401 msg = _('Below you can enter free text.\n\n [%s]') % msg
2402 )
2403 dlg.enable_user_formatting = True
2404 decision = dlg.ShowModal()
2405 text = dlg.value.strip()
2406 is_user_formatted = dlg.is_user_formatted
2407 dlg.Destroy()
2408
2409 if decision != wx.ID_SAVE:
2410 if self.debug:
2411 return self._escape(_('Text input cancelled by user.'))
2412 return self._escape('')
2413
2414
2415 if is_user_formatted:
2416 self.__cache[cache_key] = text
2417 return text
2418
2419 text = self._escape(text)
2420 self.__cache[cache_key] = text
2421 return text
2422
2423
2444
2445
2466
2467
2469 try:
2470 bill = self.__cache['bill']
2471 except KeyError:
2472 from Gnumed.wxpython import gmBillingWidgets
2473 bill = gmBillingWidgets.manage_bills(patient = self.pat)
2474 if bill is None:
2475 if self.debug:
2476 return self._escape(_('no bill selected'))
2477 return ''
2478 self.__cache['bill'] = bill
2479 self.__cache['bill-adr'] = bill.address
2480
2481 try:
2482 bill_adr = self.__cache['bill-adr']
2483 except KeyError:
2484 bill_adr = bill.address
2485 self.__cache['bill-adr'] = bill_adr
2486
2487 if bill_adr is None:
2488 if self.debug:
2489 return self._escape(_('[%s] bill has no address') % part)
2490 return ''
2491
2492 if bill_adr[part] is None:
2493 return self._escape('')
2494
2495 if data is None:
2496 return self._escape(bill_adr[part])
2497
2498 if data == '':
2499 return self._escape(bill_adr[part])
2500
2501 return data % self._escape(bill_adr[part])
2502
2503
2505 return self.__get_variant_bill_adr_part(data = data, part = 'street')
2506
2507
2509 return self.__get_variant_bill_adr_part(data = data, part = 'number')
2510
2511
2513 return self.__get_variant_bill_adr_part(data = data, part = 'subunit')
2514
2516 return self.__get_variant_bill_adr_part(data = data, part = 'urb')
2517
2518
2520 return self.__get_variant_bill_adr_part(data = data, part = 'suburb')
2521
2522
2524 return self.__get_variant_bill_adr_part(data = data, part = 'postcode')
2525
2526
2528 return self.__get_variant_bill_adr_part(data = data, part = 'l10n_region')
2529
2530
2532 return self.__get_variant_bill_adr_part(data = data, part = 'l10n_country')
2533
2534
2535
2536
2538 if self.__esc_func is None:
2539 return text
2540 return self.__esc_func(text)
2541
2542
2543 - def _escape_dict(self, the_dict=None, date_format='%Y %b %d %H:%M', none_string='', bool_strings=None):
2544 if bool_strings is None:
2545 bools = {True: _('true'), False: _('false')}
2546 else:
2547 bools = {True: bool_strings[0], False: bool_strings[1]}
2548 data = {}
2549 for field in the_dict.keys():
2550
2551
2552
2553
2554 val = the_dict[field]
2555 if val is None:
2556 data[field] = none_string
2557 continue
2558 if isinstance(val, bool):
2559 data[field] = bools[val]
2560 continue
2561 if isinstance(val, datetime.datetime):
2562 data[field] = gmDateTime.pydt_strftime(val, format = date_format)
2563 if self.__esc_style in ['latex', 'tex']:
2564 data[field] = gmTools.tex_escape_string(data[field])
2565 elif self.__esc_style in ['xetex', 'xelatex']:
2566 data[field] = gmTools.xetex_escape_string(data[field])
2567 continue
2568 try:
2569 data[field] = str(val, encoding = 'utf8', errors = 'replace')
2570 except TypeError:
2571 try:
2572 data[field] = str(val)
2573 except (UnicodeDecodeError, TypeError):
2574 val = '%s' % str(val)
2575 data[field] = val.decode('utf8', 'replace')
2576 if self.__esc_style in ['latex', 'tex']:
2577 data[field] = gmTools.tex_escape_string(data[field])
2578 elif self.__esc_style in ['xetex', 'xelatex']:
2579 data[field] = gmTools.xetex_escape_string(data[field])
2580 return data
2581
2582
2584
2585 _log.debug('testing for placeholders with pattern: %s', first_pass_placeholder_regex)
2586
2587 data_source = gmPlaceholderHandler()
2588 original_line = ''
2589
2590 while True:
2591
2592 line = wx.GetTextFromUser (
2593 _('Enter some text containing a placeholder:'),
2594 _('Testing placeholders'),
2595 centre = True,
2596 default_value = original_line
2597 )
2598 if line.strip() == '':
2599 break
2600 original_line = line
2601
2602 placeholders_in_line = regex.findall(first_pass_placeholder_regex, line, regex.IGNORECASE)
2603 if len(placeholders_in_line) == 0:
2604 continue
2605 for placeholder in placeholders_in_line:
2606 try:
2607 val = data_source[placeholder]
2608 except:
2609 val = _('error with placeholder [%s]') % placeholder
2610 if val is None:
2611 val = _('error with placeholder [%s]') % placeholder
2612 line = line.replace(placeholder, val)
2613
2614 msg = _(
2615 'Input: %s\n'
2616 '\n'
2617 'Output:\n'
2618 '%s'
2619 ) % (
2620 original_line,
2621 line
2622 )
2623 gmGuiHelpers.gm_show_info (
2624 title = _('Testing placeholders'),
2625 info = msg
2626 )
2627
2628
2630 """Functions a macro can legally use.
2631
2632 An instance of this class is passed to the GNUmed scripting
2633 listener. Hence, all actions a macro can legally take must
2634 be defined in this class. Thus we achieve some screening for
2635 security and also thread safety handling.
2636 """
2637
2638 - def __init__(self, personality = None):
2639 if personality is None:
2640 raise gmExceptions.ConstructorError('must specify personality')
2641 self.__personality = personality
2642 self.__attached = 0
2643 self._get_source_personality = None
2644 self.__user_done = False
2645 self.__user_answer = 'no answer yet'
2646 self.__pat = gmPerson.gmCurrentPatient()
2647
2648 self.__auth_cookie = str(random.random())
2649 self.__pat_lock_cookie = str(random.random())
2650 self.__lock_after_load_cookie = str(random.random())
2651
2652 _log.info('slave mode personality is [%s]', personality)
2653
2654
2655
2656 - def attach(self, personality = None):
2657 if self.__attached:
2658 _log.error('attach with [%s] rejected, already serving a client', personality)
2659 return (0, _('attach rejected, already serving a client'))
2660 if personality != self.__personality:
2661 _log.error('rejecting attach to personality [%s], only servicing [%s]' % (personality, self.__personality))
2662 return (0, _('attach to personality [%s] rejected') % personality)
2663 self.__attached = 1
2664 self.__auth_cookie = str(random.random())
2665 return (1, self.__auth_cookie)
2666
2667 - def detach(self, auth_cookie=None):
2668 if not self.__attached:
2669 return 1
2670 if auth_cookie != self.__auth_cookie:
2671 _log.error('rejecting detach() with cookie [%s]' % auth_cookie)
2672 return 0
2673 self.__attached = 0
2674 return 1
2675
2677 if not self.__attached:
2678 return 1
2679 self.__user_done = False
2680
2681 wx.CallAfter(self._force_detach)
2682 return 1
2683
2685 ver = _cfg.get(option = 'client_version')
2686 return "GNUmed %s, %s $Revision: 1.51 $" % (ver, self.__class__.__name__)
2687
2689 """Shuts down this client instance."""
2690 if not self.__attached:
2691 return 0
2692 if auth_cookie != self.__auth_cookie:
2693 _log.error('non-authenticated shutdown_gnumed()')
2694 return 0
2695 wx.CallAfter(self._shutdown_gnumed, forced)
2696 return 1
2697
2699 """Raise ourselves to the top of the desktop."""
2700 if not self.__attached:
2701 return 0
2702 if auth_cookie != self.__auth_cookie:
2703 _log.error('non-authenticated raise_gnumed()')
2704 return 0
2705 return "cMacroPrimitives.raise_gnumed() not implemented"
2706
2708 if not self.__attached:
2709 return 0
2710 if auth_cookie != self.__auth_cookie:
2711 _log.error('non-authenticated get_loaded_plugins()')
2712 return 0
2713 gb = gmGuiBroker.GuiBroker()
2714 return gb['horstspace.notebook.gui'].keys()
2715
2717 """Raise a notebook plugin within GNUmed."""
2718 if not self.__attached:
2719 return 0
2720 if auth_cookie != self.__auth_cookie:
2721 _log.error('non-authenticated raise_notebook_plugin()')
2722 return 0
2723
2724 wx.CallAfter(gmPlugin.raise_notebook_plugin, a_plugin)
2725 return 1
2726
2728 """Load external patient, perhaps create it.
2729
2730 Callers must use get_user_answer() to get status information.
2731 It is unsafe to proceed without knowing the completion state as
2732 the controlled client may be waiting for user input from a
2733 patient selection list.
2734 """
2735 if not self.__attached:
2736 return (0, _('request rejected, you are not attach()ed'))
2737 if auth_cookie != self.__auth_cookie:
2738 _log.error('non-authenticated load_patient_from_external_source()')
2739 return (0, _('rejected load_patient_from_external_source(), not authenticated'))
2740 if self.__pat.locked:
2741 _log.error('patient is locked, cannot load from external source')
2742 return (0, _('current patient is locked'))
2743 self.__user_done = False
2744 wx.CallAfter(self._load_patient_from_external_source)
2745 self.__lock_after_load_cookie = str(random.random())
2746 return (1, self.__lock_after_load_cookie)
2747
2749 if not self.__attached:
2750 return (0, _('request rejected, you are not attach()ed'))
2751 if auth_cookie != self.__auth_cookie:
2752 _log.error('non-authenticated lock_load_patient()')
2753 return (0, _('rejected lock_load_patient(), not authenticated'))
2754
2755 if lock_after_load_cookie != self.__lock_after_load_cookie:
2756 _log.warning('patient lock-after-load request rejected due to wrong cookie [%s]' % lock_after_load_cookie)
2757 return (0, 'patient lock-after-load request rejected, wrong cookie provided')
2758 self.__pat.locked = True
2759 self.__pat_lock_cookie = str(random.random())
2760 return (1, self.__pat_lock_cookie)
2761
2763 if not self.__attached:
2764 return (0, _('request rejected, you are not attach()ed'))
2765 if auth_cookie != self.__auth_cookie:
2766 _log.error('non-authenticated lock_into_patient()')
2767 return (0, _('rejected lock_into_patient(), not authenticated'))
2768 if self.__pat.locked:
2769 _log.error('patient is already locked')
2770 return (0, _('already locked into a patient'))
2771 searcher = gmPersonSearch.cPatientSearcher_SQL()
2772 if type(search_params) == dict:
2773 idents = searcher.get_identities(search_dict=search_params)
2774 raise Exception("must use dto, not search_dict")
2775 else:
2776 idents = searcher.get_identities(search_term=search_params)
2777 if idents is None:
2778 return (0, _('error searching for patient with [%s]/%s') % (search_term, search_dict))
2779 if len(idents) == 0:
2780 return (0, _('no patient found for [%s]/%s') % (search_term, search_dict))
2781
2782 if len(idents) > 1:
2783 return (0, _('several matching patients found for [%s]/%s') % (search_term, search_dict))
2784 if not gmPatSearchWidgets.set_active_patient(patient = idents[0]):
2785 return (0, _('cannot activate patient [%s] (%s/%s)') % (str(idents[0]), search_term, search_dict))
2786 self.__pat.locked = True
2787 self.__pat_lock_cookie = str(random.random())
2788 return (1, self.__pat_lock_cookie)
2789
2791 if not self.__attached:
2792 return (0, _('request rejected, you are not attach()ed'))
2793 if auth_cookie != self.__auth_cookie:
2794 _log.error('non-authenticated unlock_patient()')
2795 return (0, _('rejected unlock_patient, not authenticated'))
2796
2797 if not self.__pat.locked:
2798 return (1, '')
2799
2800 if unlock_cookie != self.__pat_lock_cookie:
2801 _log.warning('patient unlock request rejected due to wrong cookie [%s]' % unlock_cookie)
2802 return (0, 'patient unlock request rejected, wrong cookie provided')
2803 self.__pat.locked = False
2804 return (1, '')
2805
2807 if not self.__attached:
2808 return 0
2809 if auth_cookie != self.__auth_cookie:
2810 _log.error('non-authenticated select_identity()')
2811 return 0
2812 return "cMacroPrimitives.assume_staff_identity() not implemented"
2813
2815 if not self.__user_done:
2816 return (0, 'still waiting')
2817 self.__user_done = False
2818 return (1, self.__user_answer)
2819
2820
2821
2823 msg = _(
2824 'Someone tries to forcibly break the existing\n'
2825 'controlling connection. This may or may not\n'
2826 'have legitimate reasons.\n\n'
2827 'Do you want to allow breaking the connection ?'
2828 )
2829 can_break_conn = gmGuiHelpers.gm_show_question (
2830 aMessage = msg,
2831 aTitle = _('forced detach attempt')
2832 )
2833 if can_break_conn:
2834 self.__user_answer = 1
2835 else:
2836 self.__user_answer = 0
2837 self.__user_done = True
2838 if can_break_conn:
2839 self.__pat.locked = False
2840 self.__attached = 0
2841 return 1
2842
2844 top_win = wx.GetApp().GetTopWindow()
2845 if forced:
2846 top_win.Destroy()
2847 else:
2848 top_win.Close()
2849
2858
2859
2860
2861 if __name__ == '__main__':
2862
2863 if len(sys.argv) < 2:
2864 sys.exit()
2865
2866 if sys.argv[1] != 'test':
2867 sys.exit()
2868
2869 gmI18N.activate_locale()
2870 gmI18N.install_domain()
2871
2872
2874 handler = gmPlaceholderHandler()
2875 handler.debug = True
2876
2877 for placeholder in ['a', 'b']:
2878 print(handler[placeholder])
2879
2880 pat = gmPersonSearch.ask_for_patient()
2881 if pat is None:
2882 return
2883
2884 gmPatSearchWidgets.set_active_patient(patient = pat)
2885
2886 print('DOB (YYYY-MM-DD):', handler['date_of_birth::%Y-%m-%d'])
2887
2888 app = wx.PyWidgetTester(size = (200, 50))
2889
2890 ph = 'progress_notes::ap'
2891 print('%s: %s' % (ph, handler[ph]))
2892
2894
2895 tests = [
2896
2897 '$<lastname>$',
2898 '$<lastname::::3>$',
2899 '$<name::%(title)s %(firstnames)s%(preferred)s%(lastnames)s>$',
2900
2901
2902 'lastname',
2903 '$<lastname',
2904 '$<lastname::',
2905 '$<lastname::>$',
2906 '$<lastname::abc>$',
2907 '$<lastname::abc::>$',
2908 '$<lastname::abc::3>$',
2909 '$<lastname::abc::xyz>$',
2910 '$<lastname::::>$',
2911 '$<lastname::::xyz>$',
2912
2913 '$<date_of_birth::%Y-%m-%d>$',
2914 '$<date_of_birth::%Y-%m-%d::3>$',
2915 '$<date_of_birth::%Y-%m-%d::>$',
2916
2917
2918 '$<adr_location::home::35>$',
2919 '$<gender_mapper::male//female//other::5>$',
2920 '$<current_meds::==> %(product)s %(preparation)s (%(substance)s) <==\n::50>$',
2921 '$<allergy_list::%(descriptor)s, >$',
2922 '$<current_meds_table::latex//>$'
2923
2924
2925
2926
2927
2928
2929
2930
2931
2932
2933
2934
2935
2936
2937 ]
2938
2939
2940
2941
2942
2943 pat = gmPersonSearch.ask_for_patient()
2944 if pat is None:
2945 return
2946
2947 gmPatSearchWidgets.set_active_patient(patient = pat)
2948
2949 handler = gmPlaceholderHandler()
2950 handler.debug = True
2951
2952 for placeholder in tests:
2953 print(placeholder, "=>", handler[placeholder])
2954 print("--------------")
2955 input()
2956
2957
2958
2959
2960
2961
2962
2963
2964
2966 from Gnumed.pycommon import gmScriptingListener
2967 import xmlrpc.client
2968
2969 listener = gmScriptingListener.cScriptingListener(macro_executor = cMacroPrimitives(personality='unit test'), port=9999)
2970
2971 s = xmlrpc.client.ServerProxy('http://localhost:9999')
2972 print("should fail:", s.attach())
2973 print("should fail:", s.attach('wrong cookie'))
2974 print("should work:", s.version())
2975 print("should fail:", s.raise_gnumed())
2976 print("should fail:", s.raise_notebook_plugin('test plugin'))
2977 print("should fail:", s.lock_into_patient('kirk, james'))
2978 print("should fail:", s.unlock_patient())
2979 status, conn_auth = s.attach('unit test')
2980 print("should work:", status, conn_auth)
2981 print("should work:", s.version())
2982 print("should work:", s.raise_gnumed(conn_auth))
2983 status, pat_auth = s.lock_into_patient(conn_auth, 'kirk, james')
2984 print("should work:", status, pat_auth)
2985 print("should fail:", s.unlock_patient(conn_auth, 'bogus patient unlock cookie'))
2986 print("should work", s.unlock_patient(conn_auth, pat_auth))
2987 data = {'firstname': 'jame', 'lastnames': 'Kirk', 'gender': 'm'}
2988 status, pat_auth = s.lock_into_patient(conn_auth, data)
2989 print("should work:", status, pat_auth)
2990 print("should work", s.unlock_patient(conn_auth, pat_auth))
2991 print(s.detach('bogus detach cookie'))
2992 print(s.detach(conn_auth))
2993 del s
2994
2995 listener.shutdown()
2996
2998
2999 import re as regex
3000
3001 tests = [
3002 ' $<lastname>$ ',
3003 ' $<lastname::::3>$ ',
3004
3005
3006 '$<date_of_birth::%Y-%m-%d>$',
3007 '$<date_of_birth::%Y-%m-%d::3>$',
3008 '$<date_of_birth::%Y-%m-%d::>$',
3009
3010 '$<adr_location::home::35>$',
3011 '$<gender_mapper::male//female//other::5>$',
3012 '$<current_meds::==> %(product)s %(preparation)s (%(substance)s) <==\\n::50>$',
3013 '$<allergy_list::%(descriptor)s, >$',
3014
3015 '\\noindent Patient: $<lastname>$, $<firstname>$',
3016 '$<allergies::%(descriptor)s & %(l10n_type)s & {\\footnotesize %(reaction)s} \tabularnewline \hline >$',
3017 '$<current_meds:: \item[%(substance)s] {\\footnotesize (%(product)s)} %(preparation)s %(amount)s%(unit)s: %(schedule)s >$'
3018 ]
3019
3020 tests = [
3021
3022 'junk $<lastname::::3>$ junk',
3023 'junk $<lastname::abc::3>$ junk',
3024 'junk $<lastname::abc>$ junk',
3025 'junk $<lastname>$ junk',
3026
3027 'junk $<lastname>$ junk $<firstname>$ junk',
3028 'junk $<lastname::abc>$ junk $<fiststname::abc>$ junk',
3029 'junk $<lastname::abc::3>$ junk $<firstname::abc::3>$ junk',
3030 'junk $<lastname::::3>$ junk $<firstname::::3>$ junk'
3031
3032 ]
3033
3034 tests = [
3035
3036
3037
3038
3039
3040 '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',
3041 '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',
3042 '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',
3043
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::->>>$ 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::3->>>$ junk',
3046 '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',
3047 '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',
3048 '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',
3049 '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',
3050 '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',
3051 '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',
3052 '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',
3053 ]
3054
3055 tests = [
3056 'junk $<<<should pass::template::>>>$ junk',
3057 'junk $<<<should pass::template::10>>>$ junk',
3058 'junk $<<<should pass::template::10-20>>>$ junk',
3059 'junk $<<<should pass::template $<<dummy::template 2::10>>$::>>>$ junk',
3060 'junk $<<<should pass::template $<dummy::template 2::10>$::>>>$ junk',
3061
3062 'junk $<<<should pass::template::>>>$ junk $<<<should pass 2::template 2::>>>$ junk',
3063 'junk $<<<should pass::template::>>>$ junk $<<should pass 2::template 2::>>$ junk',
3064 'junk $<<<should pass::template::>>>$ junk $<should pass 2::template 2::>$ junk',
3065
3066 'junk $<<<should fail::template $<<<dummy::template 2::10>>>$::>>>$ junk',
3067
3068 'junk $<<<should fail::template::10->>>$ junk',
3069 'junk $<<<should fail::template::10->>>$ junk',
3070 'junk $<<<should fail::template::10->>>$ junk',
3071 'junk $<<<should fail::template::10->>>$ junk',
3072 'junk $<first_pass::junk $<<<3rd_pass::template::20>>>$ junk::8-10>$ junk'
3073 ]
3074
3075
3076
3077
3078
3079
3080
3081
3082
3083
3084
3085
3086
3087
3088
3089 all_tests = {
3090 first_pass_placeholder_regex: [
3091
3092 ('junk $<first_level::template::>$ junk', ['$<first_level::template::>$']),
3093 ('junk $<first_level::template::10>$ junk', ['$<first_level::template::10>$']),
3094 ('junk $<first_level::template::10-12>$ junk', ['$<first_level::template::10-12>$']),
3095
3096
3097 ('junk $<first_level::$<<insert::insert_template::0>>$::10-12>$ junk', ['$<first_level::$<<insert::insert_template::0>>$::10-12>$']),
3098 ('junk $<first_level::$<<<insert::insert_template::0>>>$::10-12>$ junk', ['$<first_level::$<<<insert::insert_template::0>>>$::10-12>$']),
3099
3100
3101 ('junk $<<second_level::$<insert::insert_template::0>$::10-12>>$ junk', ['$<insert::insert_template::0>$']),
3102 ('junk $<<<third_level::$<insert::insert_template::0>$::10-12>>>$ junk', ['$<insert::insert_template::0>$']),
3103
3104
3105 ('junk $<first_level 1::template 1::>$ junk $<<second_level 2::template 2::>>$ junk', ['$<first_level 1::template 1::>$']),
3106 ('junk $<first_level 1::template 1::>$ junk $<<<third_level 2::template 2::>>>$ junk', ['$<first_level 1::template 1::>$']),
3107
3108
3109 ('junk $<first_level 1::template 1::>$ junk $<first_level 2::template 2::>$ junk', ['$<first_level 1::template 1::>$', '$<first_level 2::template 2::>$']),
3110
3111
3112 ('returns illegal match: junk $<first_level::$<insert::insert_template::0>$::10-12>$ junk', ['$<first_level::$<insert::insert_template::0>$::10-12>$']),
3113 ],
3114 second_pass_placeholder_regex: [
3115
3116 ('junk $<<second_level::template::>>$ junk', ['$<<second_level::template::>>$']),
3117 ('junk $<<second_level::template::10>>$ junk', ['$<<second_level::template::10>>$']),
3118 ('junk $<<second_level::template::10-12>>$ junk', ['$<<second_level::template::10-12>>$']),
3119
3120
3121 ('junk $<<second_level::$<insert::insert_template::0>$::10-12>>$ junk', ['$<<second_level::$<insert::insert_template::0>$::10-12>>$']),
3122 ('junk $<<second_level::$<<<insert::insert_template::0>>>$::10-12>>$ junk', ['$<<second_level::$<<<insert::insert_template::0>>>$::10-12>>$']),
3123
3124
3125 ('junk $<first_level::$<<insert::insert_template::0>>$::10-12>$ junk', ['$<<insert::insert_template::0>>$']),
3126 ('junk $<<<third_level::$<<insert::insert_template::0>>$::10-12>>>$ junk', ['$<<insert::insert_template::0>>$']),
3127
3128
3129 ('junk $<first_level 1::template 1::>$ junk $<<second_level 2::template 2::>>$ junk', ['$<<second_level 2::template 2::>>$']),
3130 ('junk $<<second_level 1::template 1::>>$ junk $<<<third_level 2::template 2::>>>$ junk', ['$<<second_level 1::template 1::>>$']),
3131
3132
3133 ('junk $<<second_level 1::template 1::>>$ junk $<<second_level 2::template 2::>>$ junk', ['$<<second_level 1::template 1::>>$', '$<<second_level 2::template 2::>>$']),
3134
3135
3136 ('returns illegal match: junk $<<second_level::$<<insert::insert_template::0>>$::10-12>>$ junk', ['$<<second_level::$<<insert::insert_template::0>>$::10-12>>$']),
3137
3138 ],
3139 third_pass_placeholder_regex: [
3140
3141 ('junk $<<<third_level::template::>>>$ junk', ['$<<<third_level::template::>>>$']),
3142 ('junk $<<<third_level::template::10>>>$ junk', ['$<<<third_level::template::10>>>$']),
3143 ('junk $<<<third_level::template::10-12>>>$ junk', ['$<<<third_level::template::10-12>>>$']),
3144
3145
3146 ('junk $<<<third_level::$<<insert::insert_template::0>>$::10-12>>>$ junk', ['$<<<third_level::$<<insert::insert_template::0>>$::10-12>>>$']),
3147 ('junk $<<<third_level::$<insert::insert_template::0>$::10-12>>>$ junk', ['$<<<third_level::$<insert::insert_template::0>$::10-12>>>$']),
3148
3149
3150 ('junk $<<second_level::$<<<insert::insert_template::0>>>$::10-12>>$ junk', ['$<<<insert::insert_template::0>>>$']),
3151 ('junk $<first_level::$<<<insert::insert_template::0>>>$::10-12>$ junk', ['$<<<insert::insert_template::0>>>$']),
3152
3153
3154 ('junk $<first_level 1::template 1::>$ junk $<<<third_level 2::template 2::>>>$ junk', ['$<<<third_level 2::template 2::>>>$']),
3155 ('junk $<<second_level 1::template 1::>>$ junk $<<<third_level 2::template 2::>>>$ junk', ['$<<<third_level 2::template 2::>>>$']),
3156
3157
3158 ('returns illegal match: junk $<<<third_level::$<<<insert::insert_template::0>>>$::10-12>>>$ junk', ['$<<<third_level::$<<<insert::insert_template::0>>>$::10-12>>>$']),
3159 ]
3160 }
3161
3162 for pattern in [first_pass_placeholder_regex, second_pass_placeholder_regex, third_pass_placeholder_regex]:
3163 print("")
3164 print("-----------------------------")
3165 print("regex:", pattern)
3166 tests = all_tests[pattern]
3167 for t in tests:
3168 line, expected_results = t
3169 phs = regex.findall(pattern, line, regex.IGNORECASE)
3170 if len(phs) > 0:
3171 if phs == expected_results:
3172 continue
3173
3174 print("")
3175 print("failed")
3176 print("line:", line)
3177
3178 if len(phs) == 0:
3179 print("no match")
3180 continue
3181
3182 if len(phs) > 1:
3183 print("several matches")
3184 for r in expected_results:
3185 print("expected:", r)
3186 for p in phs:
3187 print("found:", p)
3188 continue
3189
3190 print("unexpected match")
3191 print("expected:", expected_results)
3192 print("found: ", phs)
3193
3194
3282
3283
3284
3292
3293
3296
3297
3298
3299 app = wx.App()
3300
3301
3302
3303
3304
3305
3306 test_placeholder()
3307
3308