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