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 u'praxis_scan2pay': u"""return scan2pay data or QR code for current praxis
475 args: <format>,
476 format: fmt=qr|txt
477 qr: QR code png file path,
478 txt: scan2pay data string,
479 default - if omitted: qr
480 """,
481
482
483 'bill': """retrieve a bill
484 args: <template>//<date format>
485 template: something %(field)s something else (do not include '//' or '::' itself in the template)
486 date format: strftime date format""",
487 'bill_scan2pay': u"""return scan2pay data or QR code for a bill
488 args: <format>,
489 format: fmt=qr|txt
490 qr: QR code png file path,
491 txt: scan2pay data string,
492 default - if omitted: qr
493 """,
494 'bill_item': """retrieve the items of a previously retrieved (and therefore cached until the next retrieval) bill
495 args: <template>//<date format>
496 template: something %(field)s something else (do not include '//' or '::' itself in the template)
497 date format: strftime date format""",
498 'bill_adr_street': "args: optional template (%s-style formatting template); cached per bill",
499 'bill_adr_number': "args: optional template (%s-style formatting template); cached per bill",
500 'bill_adr_subunit': "args: optional template (%s-style formatting template); cached per bill",
501 'bill_adr_location': "args: optional template (%s-style formatting template); cached per bill",
502 'bill_adr_suburb': "args: optional template (%s-style formatting template); cached per bill",
503 'bill_adr_postcode': "args: optional template (%s-style formatting template); cached per bill",
504 'bill_adr_region': "args: optional template (%s-style formatting template); cached per bill",
505 'bill_adr_country': "args: optional template (%s-style formatting template); cached per bill"
506
507 }
508
509 known_variant_placeholders = __known_variant_placeholders.keys()
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528 default_placeholder_regex = r'\$<[^<:]+::.*?::\d*?>\$|\$<[^<:]+::.*?::\d+-\d+>\$'
529 first_pass_placeholder_regex = r'|'.join ([
530 r'\$<[^<:]+::.*?(?=::\d*?>\$)::\d*?>\$',
531 r'\$<[^<:]+::.*?(?=::\d+-\d+>\$)::\d+-\d+>\$'
532 ])
533 second_pass_placeholder_regex = r'|'.join ([
534 r'\$<<[^<:]+?::.*?(?=::\d*?>>\$)::\d*?>>\$',
535 r'\$<<[^<:]+?::.*?(?=::\d+-\d+>>\$)::\d+-\d+>>\$'
536 ])
537 third_pass_placeholder_regex = r'|'.join ([
538 r'\$<<<[^<:]+?::.*?(?=::\d*?>>>\$)::\d*?>>>\$',
539 r'\$<<<[^<:]+?::.*?(?=::\d+-\d+>>>\$)::\d+-\d+>>>\$'
540 ])
541
542 default_placeholder_start = '$<'
543 default_placeholder_end = '>$'
544
545
547
548 fname = gmTools.get_unique_filename(prefix = 'gm-placeholders-', suffix = '.txt')
549 ph_file = io.open(fname, mode = 'wt', encoding = 'utf8', errors = 'replace')
550
551 ph_file.write('Here you can find some more documentation on placeholder use:\n')
552 ph_file.write('\n http://wiki.gnumed.de/bin/view/Gnumed/GmManualLettersForms\n\n\n')
553
554 ph_file.write('Variable placeholders:\n')
555 ph_file.write('Usage: $<PLACEHOLDER_NAME::ARGUMENTS::REGION_DEFINITION>$)\n')
556 ph_file.write(' REGION_DEFINITION:\n')
557 ph_file.write('* a single number specifying the maximum output length or\n')
558 ph_file.write('* a number, a "-", followed by a second number specifying the region of the string to return\n')
559 ph_file.write('ARGUMENTS:\n')
560 ph_file.write('* depend on the actual placeholder (see there)\n')
561 ph_file.write('* if a template is supported it will be used to %-format the output\n')
562 ph_file.write('* templates may be either %s-style or %(name)s-style\n')
563 ph_file.write('* templates cannot contain "::"\n')
564 ph_file.write('* templates cannot contain whatever the arguments divider is set to (default "//")\n')
565 for ph in known_variant_placeholders:
566 txt = __known_variant_placeholders[ph]
567 ph_file.write('\n')
568 ph_file.write(' ---=== %s ===---\n' % ph)
569 ph_file.write('\n')
570 ph_file.write(txt)
571 ph_file.write('\n\n')
572 ph_file.write('\n')
573
574 ph_file.write('Known injectable placeholders (use like: $<PLACEHOLDER_NAME::ARGUMENTS::MAX OUTPUT LENGTH>$):\n')
575 for ph in known_injectable_placeholders:
576 ph_file.write(' %s\n' % ph)
577 ph_file.write('\n')
578
579 ph_file.close()
580 gmMimeLib.call_viewer_on_file(aFile = fname, block = False)
581
582
584 """Returns values for placeholders.
585
586 - patient related placeholders operate on the currently active patient
587 - is passed to the forms handling code, for example
588
589 Return values when .debug is False:
590 - errors with placeholders return None
591 - placeholders failing to resolve to a value return an empty string
592
593 Return values when .debug is True:
594 - errors with placeholders return an error string
595 - placeholders failing to resolve to a value return a warning string
596
597 There are several types of placeholders:
598
599 injectable placeholders
600 - they must be set up before use by set_placeholder()
601 - they should be removed after use by unset_placeholder()
602 - the syntax is like extended static placeholders
603 - known ones are listed in known_injectable_placeholders
604 - per-form ones can be used but must exist before
605 the form is processed
606
607 variant placeholders
608 - those are listed in known_variant_placeholders
609 - they are parsed into placeholder, data, and maximum length
610 - the length is optional
611 - data is passed to the handler
612
613 Note that this cannot be called from a non-gui thread unless
614 wrapped in wx.CallAfter().
615 """
617
618 self.pat = gmPerson.gmCurrentPatient()
619 self.debug = False
620
621 self.invalid_placeholder_template = _('invalid placeholder >>>>>%s<<<<<')
622
623 self.__injected_placeholders = {}
624 self.__cache = {}
625
626 self.__esc_style = None
627 self.__esc_func = lambda x:x
628
629 self.__ellipsis = None
630 self.__args_divider = '//'
631 self.__data_encoding = None
632 self.__data_encoding_strict = False
633
634
635
636
638 _log.debug('setting [%s]', key)
639 if key not in known_injectable_placeholders:
640 if known_only:
641 raise ValueError('un-injectable placeholder [%s]' % key)
642
643 _log.debug('placeholder [%s] not known as injectable', key)
644
645 self.__injected_placeholders[key] = value
646
647
649 _log.debug('unsetting [%s]', key)
650 try:
651 del self.__injected_placeholders[key]
652 except KeyError:
653 _log.debug('injectable placeholder [%s] unknown', key)
654
655
657 self.__cache[key] = value
658
660 del self.__cache[key]
661
662
666
667 escape_style = property(lambda x:x, _set_escape_style)
668
669
678
679 escape_function = property(lambda x:x, _set_escape_function)
680
681
686
687 ellipsis = property(lambda x: self.__ellipsis, _set_ellipsis)
688
689
691 if divider == 'DEFAULT':
692 divider = '//'
693 self.__args_divider = divider
694
695 arguments_divider = property(lambda x: self.__args_divider, _set_arguments_divider)
696
697
699 if encoding == 'NONE':
700 self.__data_encoding = None
701 self.__data_encoding_strict = False
702
703 self.__data_encoding_strict = False
704 if encoding.endswith('-strict'):
705 self.__data_encoding_strict = True
706 encoding = encoding[:-7]
707 try:
708 codecs.lookup(encoding)
709 self.__data_encoding = encoding
710 except LookupError:
711 _log.error('<codecs> module can NOT handle encoding [%s]' % enc)
712
713 data_encoding = property(lambda x: self.__data_encoding, _set_data_encoding)
714
715
716 placeholder_regex = property(lambda x: default_placeholder_regex, lambda x:x)
717
718 first_pass_placeholder_regex = property(lambda x: first_pass_placeholder_regex, lambda x:x)
719 second_pass_placeholder_regex = property(lambda x: second_pass_placeholder_regex, lambda x:x)
720 third_pass_placeholder_regex = property(lambda x: third_pass_placeholder_regex, lambda x:x)
721
722
724 region_str = region_str.strip()
725
726 if region_str == '':
727 return None, None
728
729 try:
730 pos_last_char = int(region_str)
731 return 0, pos_last_char
732 except (TypeError, ValueError):
733 _log.debug('region definition not a simple length')
734
735
736 first_last = region_str.split('-')
737 if len(first_last) != 2:
738 _log.error('invalid placeholder region definition: %s', region_str)
739 raise ValueError
740
741 try:
742 pos_first_char = int(first_last[0].strip())
743 pos_last_char = int(first_last[1].strip())
744 except (TypeError, ValueError):
745 _log.error('invalid placeholder region definition: %s', region_str)
746 raise ValueError
747
748
749 if pos_first_char > 0:
750 pos_first_char -= 1
751
752 return pos_first_char, pos_last_char
753
754
756 if self.__data_encoding is None:
757 return data_str
758
759 try:
760 codecs.encode(data_str, self.__data_encoding, 'strict')
761 return data_str
762 except UnicodeEncodeError:
763 _log.error('cannot strict-encode string into [%s]: %s', self.__data_encoding, data_str)
764
765 if self.__data_encoding_strict:
766 return 'not compatible with encoding [%s]: %s' % (self.__data_encoding, data_str)
767
768 try:
769 import unidecode
770 except ImportError:
771 _log.debug('cannot transliterate, <unidecode> module not installed')
772 return codecs.encode(data_str, self.__data_encoding, 'replace').decode(self.__data_encoding)
773
774 return unidecode.unidecode(data_str).decode('utf8')
775
776
777
778
780 """Map self['placeholder'] to self.placeholder.
781
782 This is useful for replacing placeholders parsed out
783 of documents as strings.
784
785 Unknown/invalid placeholders still deliver a result but
786 it will be glaringly obvious if debugging is enabled.
787 """
788 _log.debug('replacing [%s]', placeholder)
789
790 original_placeholder_def = placeholder
791
792
793 if placeholder.startswith(default_placeholder_start):
794 placeholder = placeholder.lstrip('$').lstrip('<')
795 if placeholder.endswith(default_placeholder_end):
796 placeholder = placeholder.rstrip('$').rstrip('>')
797 else:
798 _log.error('placeholder must either start with [%s] and end with [%s] or neither of both', default_placeholder_start, default_placeholder_end)
799 if self.debug:
800 return self._escape(self.invalid_placeholder_template % original_placeholder_def)
801 return None
802
803
804 parts = placeholder.split('::::', 1)
805 if len(parts) == 2:
806 ph_name, region_str = parts
807 is_an_injectable = True
808 try:
809 val = self.__injected_placeholders[ph_name]
810 except KeyError:
811 is_an_injectable = False
812 except Exception:
813 _log.exception('injectable placeholder handling error: %s', original_placeholder_def)
814 if self.debug:
815 return self._escape(self.invalid_placeholder_template % original_placeholder_def)
816
817 return None
818
819 if is_an_injectable:
820 if val is None:
821 if self.debug:
822 return self._escape('injectable placeholder [%s]: no value available' % ph_name)
823 return placeholder
824 try:
825 pos_first_char, pos_last_char = self.__parse_region_definition(region_str)
826 except ValueError:
827 if self.debug:
828 return self._escape(self.invalid_placeholder_template % original_placeholder_def)
829 return None
830 if pos_last_char is None:
831 return self.__make_compatible_with_encoding(val)
832
833 if len(val) > (pos_last_char - pos_first_char):
834
835 if self.__ellipsis is not None:
836 return self.__make_compatible_with_encoding(val[pos_first_char:(pos_last_char-len(self.__ellipsis))] + self.__ellipsis)
837 return self.__make_compatible_with_encoding(val[pos_first_char:pos_last_char])
838
839
840 if len(placeholder.split('::', 2)) < 3:
841 _log.error('invalid placeholder structure: %s', original_placeholder_def)
842 if self.debug:
843 return self._escape(self.invalid_placeholder_template % original_placeholder_def)
844 return None
845
846 ph_name, data_and_lng = placeholder.split('::', 1)
847 options, region_str = data_and_lng.rsplit('::', 1)
848 _log.debug('placeholder parts: name=[%s]; region_def=[%s]; options=>>>%s<<<', ph_name, region_str, options)
849 try:
850 pos_first_char, pos_last_char = self.__parse_region_definition(region_str)
851 except ValueError:
852 if self.debug:
853 return self._escape(self.invalid_placeholder_template % original_placeholder_def)
854 return None
855
856 handler = getattr(self, '_get_variant_%s' % ph_name, None)
857 if handler is None:
858 _log.warning('no handler <_get_variant_%s> for placeholder %s', ph_name, original_placeholder_def)
859 if self.debug:
860 return self._escape(self.invalid_placeholder_template % original_placeholder_def)
861 return None
862
863 try:
864 val = handler(data = options)
865 if pos_last_char is None:
866 return self.__make_compatible_with_encoding(val)
867
868 if len(val) > (pos_last_char - pos_first_char):
869
870 if self.__ellipsis is not None:
871 return self.__make_compatible_with_encoding(val[pos_first_char:(pos_last_char-len(self.__ellipsis))] + self.__ellipsis)
872 return self.__make_compatible_with_encoding(val[pos_first_char:pos_last_char])
873
874 except Exception:
875 _log.exception('placeholder handling error: %s', original_placeholder_def)
876 if self.debug:
877 return self._escape(self.invalid_placeholder_template % original_placeholder_def)
878 return None
879
880 _log.error('something went wrong, should never get here')
881 return None
882
883
884
885
887 options = data.split('//')
888 name = options[0]
889 val = options[1]
890 if name == 'ellipsis':
891 self.ellipsis = val
892 elif name == 'argumentsdivider':
893 self.arguments_divider = val
894 elif name == 'encoding':
895 self.data_encoding = val
896 if len(options) > 2:
897 return options[2] % {'name': name, 'value': val}
898 return ''
899
901 return self._escape (
902 gmTools.coalesce (
903 _cfg.get(option = 'client_version'),
904 '%s' % self.__class__.__name__
905 )
906 )
907
936
952
954
955 select = False
956 include_descriptions = False
957 template = '%s'
958 path_template = None
959 export_path = None
960
961 data_parts = data.split(self.__args_divider)
962
963 if 'select' in data_parts:
964 select = True
965 data_parts.remove('select')
966
967 if 'description' in data_parts:
968 include_descriptions = True
969 data_parts.remove('description')
970
971 template = data_parts[0]
972
973 if len(data_parts) > 1:
974 path_template = data_parts[1]
975
976 if len(data_parts) > 2:
977 export_path = data_parts[2]
978
979
980 if export_path is not None:
981 export_path = os.path.normcase(os.path.expanduser(export_path))
982 gmTools.mkdir(export_path)
983
984
985 if select:
986 docs = gmDocumentWidgets.manage_documents(msg = _('Select the patient documents to reference from the new document.'), single_selection = False)
987 else:
988 docs = self.pat.document_folder.documents
989
990 if docs is None:
991 return ''
992
993 lines = []
994 for doc in docs:
995 lines.append(template % doc.fields_as_dict(date_format = '%Y %b %d', escape_style = self.__esc_style))
996 if include_descriptions:
997 for desc in doc.get_descriptions(max_lng = None):
998 lines.append(self._escape(desc['text'] + '\n'))
999 if path_template is not None:
1000 for part_name in doc.save_parts_to_files(export_dir = export_path):
1001 path, name = os.path.split(part_name)
1002 lines.append(path_template % {'fullpath': part_name, 'name': name})
1003
1004 return '\n'.join(lines)
1005
1007
1008 encounters = gmEncounterWidgets.select_encounters(single_selection = False)
1009 if not encounters:
1010 return ''
1011
1012 template = data
1013
1014 lines = []
1015 for enc in encounters:
1016 try:
1017 lines.append(template % enc.fields_as_dict(date_format = '%Y %b %d', escape_style = self.__esc_style))
1018 except Exception:
1019 lines.append('error formatting encounter')
1020 _log.exception('problem formatting encounter list')
1021 _log.error('template: %s', template)
1022 _log.error('encounter: %s', encounter)
1023
1024 return '\n'.join(lines)
1025
1027 """Select encounters from list and format SOAP thereof.
1028
1029 data: soap_cats (' ' -> None -> admin) // date format
1030 """
1031
1032 cats = None
1033 date_format = None
1034
1035 if data is not None:
1036 data_parts = data.split(self.__args_divider)
1037
1038
1039 if len(data_parts[0]) > 0:
1040 cats = []
1041 if ' ' in data_parts[0]:
1042 cats.append(None)
1043 data_parts[0] = data_parts[0].replace(' ', '')
1044 cats.extend(list(data_parts[0]))
1045
1046
1047 if len(data_parts) > 1:
1048 if len(data_parts[1]) > 0:
1049 date_format = data_parts[1]
1050
1051 encounters = gmEncounterWidgets.select_encounters(single_selection = False)
1052 if not encounters:
1053 return ''
1054
1055 chunks = []
1056 for enc in encounters:
1057 chunks.append(enc.format_latex (
1058 date_format = date_format,
1059 soap_cats = cats,
1060 soap_order = 'soap_rank, date'
1061 ))
1062
1063 return ''.join(chunks)
1064
1066
1067 cats = list('soapu')
1068 cats.append(None)
1069 template = '%s'
1070 interactive = True
1071 line_length = 9999
1072 time_range = None
1073
1074 if data is not None:
1075 data_parts = data.split(self.__args_divider)
1076
1077
1078 cats = []
1079
1080 for c in list(data_parts[0]):
1081 if c == ' ':
1082 c = None
1083 cats.append(c)
1084
1085 if cats == '':
1086 cats = list('soapu').append(None)
1087
1088
1089 if len(data_parts) > 1:
1090 template = data_parts[1]
1091
1092
1093 if len(data_parts) > 2:
1094 try:
1095 line_length = int(data_parts[2])
1096 except Exception:
1097 line_length = 9999
1098
1099
1100 if len(data_parts) > 3:
1101 try:
1102 time_range = 7 * int(data_parts[3])
1103 except Exception:
1104
1105
1106 time_range = data_parts[3]
1107
1108
1109 narr = self.pat.emr.get_as_journal(soap_cats = cats, time_range = time_range)
1110
1111 if len(narr) == 0:
1112 return ''
1113
1114 keys = narr[0].keys()
1115 lines = []
1116 line_dict = {}
1117 for n in narr:
1118 for key in keys:
1119 if isinstance(n[key], str):
1120 line_dict[key] = self._escape(text = n[key])
1121 continue
1122 line_dict[key] = n[key]
1123 try:
1124 lines.append((template % line_dict)[:line_length])
1125 except KeyError:
1126 return 'invalid key in template [%s], valid keys: %s]' % (template, str(keys))
1127
1128 return '\n'.join(lines)
1129
1131 return self.__get_variant_soap_by_issue_or_episode(data = data, mode = 'issue')
1132
1134 return self.__get_variant_soap_by_issue_or_episode(data = data, mode = 'episode')
1135
1137
1138
1139 cats = list('soapu')
1140 cats.append(None)
1141
1142 date_format = None
1143 template = '%s'
1144
1145 if data is not None:
1146 data_parts = data.split(self.__args_divider)
1147
1148
1149 if len(data_parts[0]) > 0:
1150 cats = []
1151 if ' ' in data_parts[0]:
1152 cats.append(None)
1153 cats.extend(list(data_parts[0].replace(' ', '')))
1154
1155
1156 if len(data_parts) > 1:
1157 if len(data_parts[1]) > 0:
1158 date_format = data_parts[1]
1159
1160
1161 if len(data_parts) > 2:
1162 if len(data_parts[2]) > 0:
1163 template = data_parts[2]
1164
1165 if mode == 'issue':
1166 narr = gmNarrativeWorkflows.select_narrative_by_issue(soap_cats = cats)
1167 else:
1168 narr = gmNarrativeWorkflows.select_narrative_by_episode(soap_cats = cats)
1169
1170 if narr is None:
1171 return ''
1172
1173 if len(narr) == 0:
1174 return ''
1175
1176 try:
1177 narr = [ template % n.fields_as_dict(date_format = date_format, escape_style = self.__esc_style) for n in narr ]
1178 except KeyError:
1179 return 'invalid key in template [%s], valid keys: %s]' % (template, str(narr[0].keys()))
1180
1181 return '\n'.join(narr)
1182
1184 return self._get_variant_soap(data = data)
1185
1187 return self._get_variant_soap(data = 's')
1188
1190 return self._get_variant_soap(data = 'o')
1191
1193 return self._get_variant_soap(data = 'a')
1194
1196 return self._get_variant_soap(data = 'p')
1197
1199 return self._get_variant_soap(data = 'u')
1200
1202 return self._get_variant_soap(data = ' ')
1203
1205
1206
1207 cats = list('soapu')
1208 cats.append(None)
1209 template = '%(narrative)s'
1210
1211 if data is not None:
1212 data_parts = data.split(self.__args_divider)
1213
1214
1215 cats = []
1216
1217 for cat in list(data_parts[0]):
1218 if cat == ' ':
1219 cat = None
1220 cats.append(cat)
1221
1222 if cats == '':
1223 cats = list('soapu')
1224 cats.append(None)
1225
1226
1227 if len(data_parts) > 1:
1228 template = data_parts[1]
1229
1230
1231 narr = gmNarrativeWorkflows.select_narrative(soap_cats = cats)
1232
1233 if narr is None:
1234 return ''
1235
1236 if len(narr) == 0:
1237 return ''
1238
1239
1240
1241
1242 if '%s' in template:
1243 narr = [ self._escape(n['narrative']) for n in narr ]
1244 else:
1245 narr = [ n.fields_as_dict(escape_style = self.__esc_style) for n in narr ]
1246
1247 try:
1248 narr = [ template % n for n in narr ]
1249 except KeyError:
1250 return 'invalid key in template [%s], valid keys: %s]' % (template, str(narr[0].keys()))
1251 except TypeError:
1252 return 'cannot mix "%%s" and "%%(field)s" in template [%s]' % template
1253
1254 return '\n'.join(narr)
1255
1256
1258 return self._get_variant_name(data = '%(title)s')
1259
1261 return self._get_variant_name(data = '%(firstnames)s')
1262
1264 return self._get_variant_name(data = '%(lastnames)s')
1265
1267 if data is None:
1268 return [_('template is missing')]
1269
1270 name = self.pat.get_active_name()
1271
1272 parts = {
1273 'title': self._escape(gmTools.coalesce(name['title'], '')),
1274 'firstnames': self._escape(name['firstnames']),
1275 'lastnames': self._escape(name['lastnames']),
1276 'preferred': self._escape(gmTools.coalesce (
1277 value2test = name['preferred'],
1278 return_instead = ' ',
1279 template4value = ' "%s" '
1280 ))
1281 }
1282
1283 return data % parts
1284
1285
1288
1289
1290
1292
1293 values = data.split('//', 2)
1294
1295 if len(values) == 2:
1296 male_value, female_value = values
1297 other_value = '<unkown gender>'
1298 elif len(values) == 3:
1299 male_value, female_value, other_value = values
1300 else:
1301 return _('invalid gender mapping layout: [%s]') % data
1302
1303 if self.pat['gender'] == 'm':
1304 return self._escape(male_value)
1305
1306 if self.pat['gender'] == 'f':
1307 return self._escape(female_value)
1308
1309 return self._escape(other_value)
1310
1311
1312
1314
1315 template = '%s'
1316 msg = _('Select the address you want to use !')
1317 cache_id = ''
1318 options = data.split('//', 4)
1319 if len(options) > 0:
1320 template = options[0]
1321 if template.strip() == '':
1322 template = '%s'
1323 if len(options) > 1:
1324 msg = options[1]
1325 if len(options) > 2:
1326 cache_id = options[2]
1327
1328 cache_key = 'generic_address::' + cache_id
1329 try:
1330 adr2use = self.__cache[cache_key]
1331 _log.debug('cache hit (%s): [%s]', cache_key, adr2use)
1332 except KeyError:
1333 adr2use = None
1334
1335 if adr2use is None:
1336 dlg = gmAddressWidgets.cAddressSelectionDlg(None, -1)
1337 dlg.message = msg
1338 choice = dlg.ShowModal()
1339 adr2use = dlg.address
1340 dlg.DestroyLater()
1341 if choice == wx.ID_CANCEL:
1342 return ''
1343 self.__cache[cache_key] = adr2use
1344
1345 return template % self._escape(adr2use[part])
1346
1348 return self.__get_variant_gen_adr_part(data = data, part = 'street')
1349
1351 return self.__get_variant_gen_adr_part(data = data, part = 'number')
1352
1354 return self.__get_variant_gen_adr_part(data = data, part = 'subunit')
1355
1357 return self.__get_variant_gen_adr_part(data = data, part = 'urb')
1358
1360 return self.__get_variant_gen_adr_part(data = data, part = 'suburb')
1361
1363 return self.__get_variant_gen_adr_part(data = data, part = 'postcode')
1364
1366 return self.__get_variant_gen_adr_part(data = data, part = 'l10n_region')
1367
1369 return self.__get_variant_gen_adr_part(data = data, part = 'l10n_country')
1370
1372
1373 template = '%s'
1374 cache_id = ''
1375 options = data.split('//', 3)
1376 if len(options) > 0:
1377 template = options[0]
1378 if template.strip() == '':
1379 template = '%s'
1380 if len(options) > 1:
1381 cache_id = options[1]
1382
1383 cache_key = 'receiver::' + cache_id
1384 try:
1385 name, adr = self.__cache[cache_key]
1386 _log.debug('cache hit (%s): [%s:%s]', cache_key, name, adr)
1387 except KeyError:
1388 name = None
1389 adr = None
1390
1391 if name is None:
1392 from Gnumed.wxpython import gmFormWidgets
1393 dlg = gmFormWidgets.cReceiverSelectionDlg(None, -1)
1394 dlg.patient = self.pat
1395 choice = dlg.ShowModal()
1396 name = dlg.name
1397 adr = dlg.address
1398 dlg.DestroyLater()
1399 if choice == wx.ID_CANCEL:
1400 return ''
1401 self.__cache[cache_key] = (name, adr)
1402
1403 if part == 'name':
1404 return template % self._escape(name)
1405
1406 return template % self._escape(gmTools.coalesce(adr[part], ''))
1407
1409 return self.__get_variant_receiver_part(data = data, part = 'name')
1410
1412 return self.__get_variant_receiver_part(data = data, part = 'street')
1413
1415 return self.__get_variant_receiver_part(data = data, part = 'number')
1416
1418 return self.__get_variant_receiver_part(data = data, part = 'subunit')
1419
1421 return self.__get_variant_receiver_part(data = data, part = 'urb')
1422
1424 return self.__get_variant_receiver_part(data = data, part = 'suburb')
1425
1427 return self.__get_variant_receiver_part(data = data, part = 'postcode')
1428
1430 return self.__get_variant_receiver_part(data = data, part = 'l10n_region')
1431
1433 return self.__get_variant_receiver_part(data = data, part = 'l10n_country')
1434
1436
1437 data_parts = data.split(self.__args_divider)
1438
1439
1440 adr_type = data_parts[0].strip()
1441 orig_type = adr_type
1442 if adr_type != '':
1443 adrs = self.pat.get_addresses(address_type = adr_type)
1444 if len(adrs) == 0:
1445 _log.warning('no address for type [%s]', adr_type)
1446 adr_type = ''
1447 if adr_type == '':
1448 _log.debug('asking user for address type')
1449 adr = gmPersonContactWidgets.select_address(missing = orig_type, person = self.pat)
1450 if adr is None:
1451 if self.debug:
1452 return _('no address type replacement selected')
1453 return ''
1454 adr_type = adr['address_type']
1455 adr = self.pat.get_addresses(address_type = adr_type)[0]
1456
1457
1458 template = _('%(street)s %(number)s, %(postcode)s %(urb)s, %(l10n_region)s, %(l10n_country)s')
1459 if len(data_parts) > 1:
1460 if data_parts[1].strip() != '':
1461 template = data_parts[1]
1462
1463 try:
1464 return template % adr.fields_as_dict(escape_style = self.__esc_style)
1465 except Exception:
1466 _log.exception('error formatting address')
1467 _log.error('template: %s', template)
1468
1469 return None
1470
1472 requested_type = data.strip()
1473 cache_key = 'adr-type-%s' % requested_type
1474 try:
1475 type2use = self.__cache[cache_key]
1476 _log.debug('cache hit (%s): [%s] -> [%s]', cache_key, requested_type, type2use)
1477 except KeyError:
1478 type2use = requested_type
1479 if type2use != '':
1480 adrs = self.pat.get_addresses(address_type = type2use)
1481 if len(adrs) == 0:
1482 _log.warning('no address of type [%s] for <%s> field extraction', requested_type, part)
1483 type2use = ''
1484 if type2use == '':
1485 _log.debug('asking user for replacement address type')
1486 adr = gmPersonContactWidgets.select_address(missing = requested_type, person = self.pat)
1487 if adr is None:
1488 _log.debug('no replacement selected')
1489 if self.debug:
1490 return self._escape(_('no address type replacement selected'))
1491 return ''
1492 type2use = adr['address_type']
1493 self.__cache[cache_key] = type2use
1494 _log.debug('caching (%s): [%s] -> [%s]', cache_key, requested_type, type2use)
1495
1496 part_data = self.pat.get_addresses(address_type = type2use)[0][part]
1497 if part_data is None:
1498 part_data = ''
1499 return self._escape(part_data)
1500
1501
1503 return self.__get_variant_adr_part(data = data, part = 'street')
1504
1506 return self.__get_variant_adr_part(data = data, part = 'number')
1507
1509 return self.__get_variant_adr_part(data = data, part = 'subunit')
1510
1512 return self.__get_variant_adr_part(data = data, part = 'urb')
1513
1515 return self.__get_variant_adr_part(data = data, part = 'suburb')
1516
1517 - def _get_variant_adr_postcode(self, data='?'):
1518 return self.__get_variant_adr_part(data = data, part = 'postcode')
1519
1521 return self.__get_variant_adr_part(data = data, part = 'l10n_region')
1522
1524 return self.__get_variant_adr_part(data = data, part = 'l10n_country')
1525
1527 comm_type = None
1528 template = '%(url)s'
1529 if data is not None:
1530 data_parts = data.split(self.__args_divider)
1531 if len(data_parts) > 0:
1532 comm_type = data_parts[0]
1533 if len(data_parts) > 1:
1534 template = data_parts[1]
1535
1536 comms = self.pat.get_comm_channels(comm_medium = comm_type)
1537 if len(comms) == 0:
1538 if self.debug:
1539 return self._escape(_('no URL for comm channel [%s]') % data)
1540 return ''
1541
1542 return template % comms[0].fields_as_dict(escape_style = self.__esc_style)
1543
1545
1546 template = '%s'
1547 target_mime = None
1548 target_ext = None
1549 if data is not None:
1550 parts = data.split(self.__args_divider)
1551 template = parts[0]
1552 if len(parts) > 1:
1553 target_mime = parts[1].strip()
1554 if len(parts) > 2:
1555 target_ext = parts[2].strip()
1556 if target_ext is None:
1557 if target_mime is not None:
1558 target_ext = gmMimeLib.guess_ext_by_mimetype(mimetype = target_mime)
1559
1560 cache_key = 'patient_photo_path::%s::%s' % (target_mime, target_ext)
1561 try:
1562 fname = self.__cache[cache_key]
1563 _log.debug('cache hit on [%s]: %s', cache_key, fname)
1564 except KeyError:
1565 mugshot = self.pat.document_folder.latest_mugshot
1566 if mugshot is None:
1567 if self.debug:
1568 return self._escape(_('no mugshot available'))
1569 return ''
1570 fname = mugshot.save_to_file (
1571 target_mime = target_mime,
1572 target_extension = target_ext,
1573 ignore_conversion_problems = True
1574 )
1575 if fname is None:
1576 if self.debug:
1577 return self._escape(_('cannot export or convert latest mugshot'))
1578 return ''
1579 self.__cache[cache_key] = fname
1580
1581 return template % fname
1582
1583
1585 options = data.split(self.__args_divider)
1586 template = options[0].strip()
1587 if template == '':
1588 template = '%s'
1589
1590 return template % self.pat.export_as_vcard()
1591
1592
1594 template = u'%s'
1595 format = 'txt'
1596 options = data.split(self.__args_divider)
1597 _log.debug('options: %s', options)
1598 for o in options:
1599 if o.strip().startswith('fmt='):
1600 format = o.strip()[4:]
1601 if format not in ['qr', 'mcf', 'txt']:
1602 return self._escape(_('patient_mcf: invalid format (qr/mcf/txt)'))
1603 continue
1604 if o.strip().startswith('tmpl='):
1605 template = o.strip()[5:]
1606 continue
1607 _log.debug('template: %s' % template)
1608 _log.debug('format: %s' % format)
1609
1610 if format == 'txt':
1611 return template % self._escape(self.pat.MECARD)
1612
1613 if format == 'mcf':
1614 return template % self.pat.export_as_mecard()
1615
1616 if format == 'qr':
1617 qr_filename = gmTools.create_qrcode(text = self.pat.MECARD)
1618 if qr_filename is None:
1619 return self._escape('patient_mcf-cannot_create_QR_code')
1620 return template % qr_filename
1621
1622 return None
1623
1624
1626 options = data.split(self.__args_divider)
1627 template = options[0].strip()
1628 if template == '':
1629 template = '%s'
1630
1631 return template % self.pat.export_as_gdt()
1632
1633
1650
1651
1652
1653
1654
1655
1657 options = data.split(self.__args_divider)
1658
1659 if 'select' in options:
1660 options.remove('select')
1661 branch = 'select branch'
1662 else:
1663 branch = gmPraxis.cPraxisBranch(aPK_obj = gmPraxis.gmCurrentPraxisBranch()['pk_praxis_branch'])
1664
1665 template = '%s'
1666 if len(options) > 0:
1667 template = options[0]
1668 if template.strip() == '':
1669 template = '%s'
1670
1671 return template % branch.fields_as_dict(escape_style = self.__esc_style)
1672
1673
1675
1676 cache_key = 'current_branch_vcf_path'
1677 try:
1678 vcf_name = self.__cache[cache_key]
1679 _log.debug('cache hit (%s): [%s]', cache_key, vcf_name)
1680 except KeyError:
1681 vcf_name = gmPraxis.gmCurrentPraxisBranch().vcf
1682 self.__cache[cache_key] = vcf_name
1683
1684 template = '%s'
1685 if data.strip() != '':
1686 template = data
1687
1688 return template % vcf_name
1689
1690
1692 template = u'%s'
1693 format = 'txt'
1694 options = data.split(self.__args_divider)
1695 _log.debug('options: %s', options)
1696 for o in options:
1697 if o.strip().startswith('fmt='):
1698 format = o.strip()[4:]
1699 if format not in ['qr', 'mcf', 'txt']:
1700 return self._escape(_('praxis_mcf: invalid format (qr/mcf/txt)'))
1701 continue
1702 if o.strip().startswith('tmpl='):
1703 template = o.strip()[5:]
1704 continue
1705 _log.debug('template: %s' % template)
1706 _log.debug('format: %s' % format)
1707
1708 if format == 'txt':
1709 return template % self._escape(gmPraxis.gmCurrentPraxisBranch().MECARD)
1710
1711 if format == 'mcf':
1712 return template % gmPraxis.gmCurrentPraxisBranch().export_as_mecard()
1713
1714 if format == 'qr':
1715 qr_filename = gmTools.create_qrcode(text = gmPraxis.gmCurrentPraxisBranch().MECARD)
1716 if qr_filename is None:
1717 return self._escape('praxis_mcf-cannot_create_QR_code')
1718 return template % qr_filename
1719
1720 return None
1721
1722
1724
1725 format = 'qr'
1726 options = data.split(self.__args_divider)
1727 _log.debug('options: %s', options)
1728 for o in options:
1729 if o.strip().startswith('fmt='):
1730 format = o.strip()[4:]
1731 if format not in ['qr', 'txt']:
1732 return self._escape(_('praxis_scan2pay: invalid format (qr/txt)'))
1733 continue
1734
1735
1736
1737
1738 _log.debug('format: %s' % format)
1739
1740 data_str = gmPraxis.gmCurrentPraxisBranch().scan2pay_data
1741 if data_str is None:
1742 if self.debug:
1743 return self._escape('praxis_scan2pay-cannot_create_data_file')
1744 return ''
1745
1746 if format == 'txt':
1747 return self._escape(data_str)
1748
1749
1750 if format == 'qr':
1751 qr_filename = gmTools.create_qrcode(text = data_str)
1752 if qr_filename is None:
1753 if self.debug:
1754 return self._escape('praxis_scan2pay-cannot_create_QR_code')
1755 return ''
1756
1757 return qr_filename
1758
1759 return None
1760
1761
1763 options = data.split(self.__args_divider)
1764
1765
1766 template = _('%(street)s %(number)s, %(postcode)s %(urb)s, %(l10n_region)s, %(l10n_country)s')
1767 if len(options) > 0:
1768 if options[0].strip() != '':
1769 template = options[0]
1770
1771 adr = gmPraxis.gmCurrentPraxisBranch().address
1772 if adr is None:
1773 if self.debug:
1774 return _('no address recorded')
1775 return ''
1776 try:
1777 return template % adr.fields_as_dict(escape_style = self.__esc_style)
1778 except Exception:
1779 _log.exception('error formatting address')
1780 _log.error('template: %s', template)
1781
1782 return None
1783
1784
1786 options = data.split(self.__args_divider)
1787 comm_type = options[0]
1788 template = '%(url)s'
1789 if len(options) > 1:
1790 template = options[1]
1791
1792 comms = gmPraxis.gmCurrentPraxisBranch().get_comm_channels(comm_medium = comm_type)
1793 if len(comms) == 0:
1794 if self.debug:
1795 return self._escape(_('no URL for comm channel [%s]') % data)
1796 return ''
1797
1798 return template % comms[0].fields_as_dict(escape_style = self.__esc_style)
1799
1800
1802 options = data.split(self.__args_divider)
1803 id_type = options[0].strip()
1804 if id_type == '':
1805 return self._escape('praxis external ID: type is missing')
1806
1807 if len(options) > 1:
1808 issuer = options[1].strip()
1809 if issuer == '':
1810 issue = None
1811 else:
1812 issuer = None
1813
1814 if len(options) > 2:
1815 template = options[2]
1816 else:
1817 template = '%(name)s: %(value)s (%(issuer)s)'
1818
1819 ids = gmPraxis.gmCurrentPraxisBranch().get_external_ids(id_type = id_type, issuer = issuer)
1820 if len(ids) == 0:
1821 if self.debug:
1822 return self._escape(_('no external ID [%s] by [%s]') % (id_type, issuer))
1823 return ''
1824
1825 return template % self._escape_dict(the_dict = ids[0], none_string = '')
1826
1827
1828
1829
1831 prov = gmStaff.gmCurrentProvider()
1832
1833 tmp = '%s%s. %s' % (
1834 gmTools.coalesce(prov['title'], '', '%s '),
1835 prov['firstnames'][:1],
1836 prov['lastnames']
1837 )
1838 return self._escape(tmp)
1839
1840
1842 if data is None:
1843 template = u'%(title)s'
1844 elif data.strip() == u'':
1845 data = u'%(title)s'
1846 return self._get_variant_current_provider_name(data = data)
1847
1848
1850 if data is None:
1851 data = u'%(firstnames)s'
1852 elif data.strip() == u'':
1853 data = u'%(firstnames)s'
1854 return self._get_variant_current_provider_name(data = data)
1855
1856
1858 if data is None:
1859 data = u'%(lastnames)s'
1860 elif data.strip() == u'':
1861 data = u'%(lastnames)s'
1862 return self._get_variant_current_provider_name(data = data)
1863
1864
1878
1879
1881 data_parts = data.split(self.__args_divider)
1882 if len(data_parts) < 2:
1883 return self._escape('current provider external ID: template is missing')
1884
1885 id_type = data_parts[0].strip()
1886 if id_type == '':
1887 return self._escape('current provider external ID: type is missing')
1888
1889 issuer = data_parts[1].strip()
1890 if issuer == '':
1891 return self._escape('current provider external ID: issuer is missing')
1892
1893 prov = gmStaff.gmCurrentProvider()
1894 ids = prov.identity.get_external_ids(id_type = id_type, issuer = issuer)
1895
1896 if len(ids) == 0:
1897 if self.debug:
1898 return self._escape(_('no external ID [%s] by [%s]') % (id_type, issuer))
1899 return ''
1900
1901 return self._escape(ids[0]['value'])
1902
1903
1905 prov = self.pat.primary_provider
1906 if prov is None:
1907 return self._get_variant_current_provider()
1908
1909 title = gmTools.coalesce (
1910 prov['title'],
1911 gmPerson.map_gender2salutation(prov['gender'])
1912 )
1913
1914 tmp = '%s %s. %s' % (
1915 title,
1916 prov['firstnames'][:1],
1917 prov['lastnames']
1918 )
1919 return self._escape(tmp)
1920
1921
1923 data_parts = data.split(self.__args_divider)
1924 if len(data_parts) < 2:
1925 return self._escape('primary in-praxis provider external ID: template is missing')
1926
1927 id_type = data_parts[0].strip()
1928 if id_type == '':
1929 return self._escape('primary in-praxis provider external ID: type is missing')
1930
1931 issuer = data_parts[1].strip()
1932 if issuer == '':
1933 return self._escape('primary in-praxis provider external ID: issuer is missing')
1934
1935 prov = self.pat.primary_provider
1936 if prov is None:
1937 if self.debug:
1938 return self._escape(_('no primary in-praxis provider'))
1939 return ''
1940
1941 ids = prov.identity.get_external_ids(id_type = id_type, issuer = issuer)
1942
1943 if len(ids) == 0:
1944 if self.debug:
1945 return self._escape(_('no external ID [%s] by [%s]') % (id_type, issuer))
1946 return ''
1947
1948 return self._escape(ids[0]['value'])
1949
1950
1952 data_parts = data.split(self.__args_divider)
1953 if len(data_parts) < 2:
1954 return self._escape('patient external ID: template is missing')
1955
1956 id_type = data_parts[0].strip()
1957 if id_type == '':
1958 return self._escape('patient external ID: type is missing')
1959
1960 issuer = data_parts[1].strip()
1961 if issuer == '':
1962 return self._escape('patient external ID: issuer is missing')
1963
1964 ids = self.pat.get_external_ids(id_type = id_type, issuer = issuer)
1965
1966 if len(ids) == 0:
1967 if self.debug:
1968 return self._escape(_('no external ID [%s] by [%s]') % (id_type, issuer))
1969 return ''
1970
1971 return self._escape(ids[0]['value'])
1972
1973
1975 allg_state = self.pat.emr.allergy_state
1976
1977 if allg_state['last_confirmed'] is None:
1978 date_confirmed = ''
1979 else:
1980 date_confirmed = ' (%s)' % gmDateTime.pydt_strftime (
1981 allg_state['last_confirmed'],
1982 format = '%Y %B %d'
1983 )
1984
1985 tmp = '%s%s' % (
1986 allg_state.state_string,
1987 date_confirmed
1988 )
1989 return self._escape(tmp)
1990
1991
1999
2000
2007
2008
2010 return self._get_variant_current_meds_AMTS(data=data, strict=False)
2011
2012
2014
2015
2016 emr = self.pat.emr
2017 from Gnumed.wxpython import gmMedicationWidgets
2018 intakes2export = gmMedicationWidgets.manage_substance_intakes(emr = emr)
2019 if intakes2export is None:
2020 return ''
2021 if len(intakes2export) == 0:
2022 return ''
2023
2024
2025 unique_intakes = {}
2026 for intake in intakes2export:
2027 if intake['pk_drug_product'] is None:
2028 unique_intakes[intake['pk_substance']] = intake
2029 else:
2030 unique_intakes[intake['product']] = intake
2031 del intakes2export
2032 unique_intakes = unique_intakes.values()
2033
2034
2035 self.__create_amts_datamatrix_files(intakes = unique_intakes)
2036
2037
2038 intake_as_latex_rows = []
2039 for intake in unique_intakes:
2040 intake_as_latex_rows.append(intake._get_as_amts_latex(strict = strict))
2041 del unique_intakes
2042
2043
2044
2045 intake_as_latex_rows.extend(emr.allergy_state._get_as_amts_latex(strict = strict))
2046
2047 for allg in emr.get_allergies():
2048 intake_as_latex_rows.append(allg._get_as_amts_latex(strict = strict))
2049
2050
2051 table_rows = intake_as_latex_rows[:15]
2052 if len(intake_as_latex_rows) > 15:
2053 table_rows.append('\\newpage')
2054 table_rows.extend(intake_as_latex_rows[15:30])
2055 if len(intake_as_latex_rows) > 30:
2056 table_rows.append('\\newpage')
2057 table_rows.extend(intake_as_latex_rows[30:45])
2058
2059 if strict:
2060 return '\n'.join(table_rows)
2061
2062
2063 if len(intake_as_latex_rows) > 45:
2064 table_rows.append('\\newpage')
2065 table_rows.extend(intake_as_latex_rows[30:45])
2066
2067 if len(intake_as_latex_rows) > 60:
2068 table_rows.append('\\newpage')
2069 table_rows.extend(intake_as_latex_rows[30:45])
2070
2071 return '\n'.join(table_rows)
2072
2073
2075
2076
2077 for idx in [1,2,3]:
2078 self.set_placeholder(key = 'amts_data_file_%s' % idx, value = './missing-file.txt', known_only = False)
2079 self.set_placeholder(key = 'amts_png_file_%s' % idx, value = './missing-file.png', known_only = False)
2080 self.set_placeholder(key = 'amts_png_file_current_page', value = './missing-file-current-page.png', known_only = False)
2081 self.set_placeholder(key = 'amts_png_file_utf8', value = './missing-file-utf8.png', known_only = False)
2082 self.set_placeholder(key = 'amts_data_file_utf8', value = './missing-file-utf8.txt', known_only = False)
2083
2084
2085 found, dmtx_creator = gmShellAPI.detect_external_binary(binary = 'gm-create_datamatrix')
2086 _log.debug(dmtx_creator)
2087 if not found:
2088 _log.error('gm-create_datamatrix(.bat/.exe) not found')
2089 return
2090
2091 png_dir = gmTools.mk_sandbox_dir()
2092 _log.debug('sandboxing AMTS datamatrix PNGs in: %s', png_dir)
2093
2094 from Gnumed.business import gmForms
2095
2096
2097
2098 amts_data_template_def_file = gmMedication.generate_amts_data_template_definition_file(strict = False)
2099 _log.debug('amts data template definition file: %s', amts_data_template_def_file)
2100 form = gmForms.cTextForm(template_file = amts_data_template_def_file)
2101
2102 amts_sections = '<S>%s</S>' % ''.join ([
2103 i._get_as_amts_data(strict = False) for i in intakes
2104 ])
2105
2106 emr = self.pat.emr
2107 amts_sections += emr.allergy_state._get_as_amts_data(strict = False) % ''.join ([
2108 a._get_as_amts_data(strict = False) for a in emr.get_allergies()
2109 ])
2110 self.set_placeholder(key = 'amts_intakes_as_data_enhanced', value = amts_sections, known_only = False)
2111
2112 self.set_placeholder(key = 'amts_total_pages', value = '1', known_only = False)
2113 success = form.substitute_placeholders(data_source = self)
2114 self.unset_placeholder(key = 'amts_intakes_as_data_enhanced')
2115
2116 self.unset_placeholder(key = 'amts_total_pages')
2117 if not success:
2118 _log.error('cannot substitute into amts data file form template')
2119 return
2120 data_file = form.re_editable_filenames[0]
2121 png_file = os.path.join(png_dir, 'gm4amts-datamatrix-utf8.png')
2122 cmd = '%s %s %s' % (dmtx_creator, 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 self.set_placeholder(key = 'amts_data_file_utf8', value = data_file, known_only = False)
2128 self.set_placeholder(key = 'amts_png_file_utf8', value = png_file, known_only = False)
2129
2130
2131 total_pages = (len(intakes) / 15.0)
2132 if total_pages > int(total_pages):
2133 total_pages += 1
2134 total_pages = int(total_pages)
2135 _log.debug('total pages: %s', total_pages)
2136
2137 png_file_base = os.path.join(png_dir, 'gm4amts-datamatrix-page-')
2138 for this_page in range(1,total_pages+1):
2139 intakes_this_page = intakes[(this_page-1)*15:this_page*15]
2140 amts_data_template_def_file = gmMedication.generate_amts_data_template_definition_file(strict = True)
2141 _log.debug('amts data template definition file: %s', amts_data_template_def_file)
2142 form = gmForms.cTextForm(template_file = amts_data_template_def_file)
2143
2144 amts_sections = '<S>%s</S>' % ''.join ([
2145 i._get_as_amts_data(strict = False) for i in intakes_this_page
2146 ])
2147 if this_page == total_pages:
2148
2149 emr = self.pat.emr
2150 amts_sections += emr.allergy_state._get_as_amts_data(strict = False) % ''.join ([
2151 a._get_as_amts_data(strict = False) for a in emr.get_allergies()
2152 ])
2153 self.set_placeholder(key = 'amts_intakes_as_data', value = amts_sections, known_only = False)
2154
2155 if total_pages == 1:
2156 pg_idx = ''
2157 else:
2158 pg_idx = '%s' % this_page
2159 self.set_placeholder(key = 'amts_page_idx', value = pg_idx, known_only = False)
2160 self.set_placeholder(key = 'amts_total_pages', value = '%s' % total_pages, known_only = False)
2161 success = form.substitute_placeholders(data_source = self)
2162 self.unset_placeholder(key = 'amts_intakes_as_data')
2163
2164 self.unset_placeholder(key = 'amts_page_idx')
2165 self.unset_placeholder(key = 'amts_total_pages')
2166 if not success:
2167 _log.error('cannot substitute into amts data file form template')
2168 return
2169
2170 data_file = form.re_editable_filenames[0]
2171 png_file = '%s%s.png' % (png_file_base, this_page)
2172 latin1_data_file = gmTools.recode_file (
2173 source_file = data_file,
2174 source_encoding = 'utf8',
2175 target_encoding = 'latin1',
2176 base_dir = os.path.split(data_file)[0]
2177 )
2178 cmd = '%s %s %s' % (dmtx_creator, latin1_data_file, png_file)
2179 success = gmShellAPI.run_command_in_shell(command = cmd, blocking = True)
2180 if not success:
2181 _log.error('error running [%s]' % cmd)
2182 return
2183
2184
2185 self.set_placeholder(key = 'amts_data_file_%s' % this_page, value = latin1_data_file, known_only = False)
2186 self.set_placeholder(key = 'amts_png_file_%s' % this_page, value = png_file, known_only = False)
2187
2188 self.set_placeholder(key = 'amts_png_file_current_page', value = png_file_base + '\\thepage', known_only = False)
2189
2190
2192 if data is None:
2193 return self._escape(_('current_meds_for_rx: template is missing'))
2194
2195 emr = self.pat.emr
2196 from Gnumed.wxpython import gmMedicationWidgets
2197 current_meds = gmMedicationWidgets.manage_substance_intakes(emr = emr)
2198 if current_meds is None:
2199 return ''
2200
2201 intakes2show = {}
2202 for intake in current_meds:
2203 fields_dict = intake.fields_as_dict(date_format = '%Y %b %d', escape_style = self.__esc_style)
2204 fields_dict['medically_formatted_start'] = self._escape(intake.medically_formatted_start)
2205 if intake['pk_drug_product'] is None:
2206 fields_dict['product'] = self._escape(_('generic %s') % fields_dict['substance'])
2207 fields_dict['contains'] = self._escape('%s %s%s' % (fields_dict['substance'], fields_dict['amount'], fields_dict['unit']))
2208 intakes2show[fields_dict['product']] = fields_dict
2209 else:
2210 comps = [ c.split('::') for c in intake.containing_drug['components'] ]
2211 fields_dict['contains'] = self._escape('; '.join([ '%s %s%s' % (c[0], c[1], c[2]) for c in comps ]))
2212 intakes2show[intake['product']] = fields_dict
2213
2214 intakes2dispense = {}
2215 for product, intake in intakes2show.items():
2216 msg = _('Dispense how much/many of "%(product)s (%(contains)s)" ?') % intake
2217 amount2dispense = wx.GetTextFromUser(msg, _('Amount to dispense ?'))
2218 if amount2dispense == '':
2219 continue
2220 intake['amount2dispense'] = amount2dispense
2221 intakes2dispense[product] = intake
2222
2223 return '\n'.join([ data % intake for intake in intakes2dispense.values() ])
2224
2225
2240
2241
2275
2282
2289
2295
2327
2333
2335 options = data.split(self.__args_divider)
2336 template = options[0]
2337 if len(options) > 1:
2338 date_format = options[1]
2339 else:
2340 date_format = '%Y %b %d'
2341 vaccinations_as_dict = []
2342 for v in self.pat.emr.get_vaccinations(order_by = 'date_given DESC, vaccine'):
2343 v_as_dict = v.fields_as_dict(date_format = date_format, escape_style = self.__esc_style)
2344 v_as_dict['l10n_indications'] = [ ind['l10n_indication'] for ind in v['indications'] ]
2345 vaccinations_as_dict.append(v_as_dict)
2346
2347 return u'\n'.join([ template % v for v in vaccinations_as_dict ])
2348
2349
2351
2352 if data is None:
2353 if self.debug:
2354 _log.error('PHX: missing placeholder arguments')
2355 return self._escape(_('PHX: Invalid placeholder options.'))
2356 return ''
2357
2358 _log.debug('arguments: %s', data)
2359
2360 data_parts = data.split(self.__args_divider)
2361 template = '%s'
2362 separator = '\n'
2363 date_format = '%Y %b %d'
2364 try:
2365 template = data_parts[0]
2366 separator = data_parts[1]
2367 date_format = data_parts[2]
2368 except IndexError:
2369 pass
2370
2371 phxs = gmEMRStructWidgets.select_health_issues(emr = self.pat.emr)
2372 if phxs is None:
2373 if self.debug:
2374 return self._escape(_('no PHX for this patient (available or selected)'))
2375 return ''
2376
2377 return separator.join ([
2378 template % phx.fields_as_dict (
2379 date_format = date_format,
2380 escape_style = self.__esc_style,
2381 bool_strings = (self._escape(_('yes')), self._escape(_('no')))
2382 ) for phx in phxs
2383 ])
2384
2385
2392
2393
2395
2396 if data is None:
2397 return self._escape(_('template is missing'))
2398 template = data
2399 dxs = self.pat.emr.candidate_diagnoses
2400 if len(dxs) == 0:
2401 _log.debug('no diagnoses available')
2402 return ''
2403 selected = gmListWidgets.get_choices_from_list (
2404 msg = _('Select the relevant diagnoses:'),
2405 caption = _('Diagnosis selection'),
2406 columns = [ _('Diagnosis'), _('Marked confidential'), _('Certainty'), _('Source') ],
2407 choices = [[
2408 dx['diagnosis'],
2409 gmTools.bool2subst(dx['explicitely_confidential'], _('yes'), _('no'), _('unknown')),
2410 gmTools.coalesce(dx['diagnostic_certainty_classification'], ''),
2411 dx['source']
2412 ] for dx in dxs
2413 ],
2414 data = dxs,
2415 single_selection = False,
2416 can_return_empty = True
2417 )
2418 if selected is None:
2419 _log.debug('user did not select any diagnoses')
2420 return ''
2421 if len(selected) == 0:
2422 _log.debug('user did not select any diagnoses')
2423 return ''
2424
2425 return '\n'.join(template % self._escape_dict(dx, none_string = '?', bool_strings = [_('yes'), _('no')]) for dx in selected)
2426
2427
2430
2431
2434
2435
2437 return self._escape(urllib.parse.quote(data.encode('utf8')))
2438
2439
2440 - def _get_variant_text_snippet(self, data=None):
2441 data_parts = data.split(self.__args_divider)
2442 keyword = data_parts[0]
2443 template = '%s'
2444 if len(data_parts) > 1:
2445 template = data_parts[1]
2446
2447 expansion = gmKeywordExpansionWidgets.expand_keyword(keyword = keyword, show_list_if_needed = True)
2448
2449 if expansion is None:
2450 if self.debug:
2451 return self._escape(_('no textual expansion found for keyword <%s>') % keyword)
2452 return ''
2453
2454
2455 return template % expansion
2456
2457
2459 parts = data.split(self.__args_divider)
2460 keyword = parts[0]
2461 template = '%s'
2462 target_mime = None
2463 target_ext = None
2464 if len(parts) > 1:
2465 template = parts[1]
2466 if len(parts) > 2:
2467 if parts[2].strip() != '':
2468 target_mime = parts[2].strip()
2469 if len(parts) > 3:
2470 if parts[3].strip() != '':
2471 target_ext = parts[3].strip()
2472
2473 expansion = gmKeywordExpansion.get_expansion (
2474 keyword = keyword,
2475 textual_only = False,
2476 binary_only = True
2477 )
2478 if expansion is None:
2479 if self.debug:
2480 return self._escape(_('no binary expansion found for keyword <%s>') % keyword)
2481 return ''
2482
2483 saved_fname = expansion.save_to_file()
2484 if saved_fname is None:
2485 if self.debug:
2486 return self._escape(_('cannot export data of binary expansion keyword <%s>') % keyword)
2487 return ''
2488
2489 if expansion['is_encrypted']:
2490 pwd = wx.GetPasswordFromUser (
2491 message = _('Enter your GnuPG passphrase for decryption of [%s]') % expansion['keyword'],
2492 caption = _('GnuPG passphrase prompt'),
2493 default_value = ''
2494 )
2495 saved_fname = gmCrypto.gpg_decrypt_file(filename = saved_fname, passphrase = pwd)
2496 if saved_fname is None:
2497 if self.debug:
2498 return self._escape(_('cannot decrypt data of binary expansion keyword <%s>') % keyword)
2499 return ''
2500
2501 if target_mime is None:
2502 return template % saved_fname
2503
2504 converted_fname = gmMimeLib.convert_file(filename = saved_fname, target_mime = target_mime, target_extension = target_ext)
2505 if converted_fname is None:
2506 if self.debug:
2507 return self._escape(_('cannot convert data of binary expansion keyword <%s>') % keyword)
2508
2509 return template % saved_fname
2510
2511 return template % converted_fname
2512
2513
2515 options = data.split(self.__args_divider)
2516 if len(options) == 0:
2517 return None
2518 text4qr = options[0]
2519 if len(options) > 1:
2520 template = options[1]
2521 else:
2522 template = u'%s'
2523 qr_filename = gmTools.create_qrcode(text = text4qr)
2524 if qr_filename is None:
2525 return self._escape('cannot_create_QR_code')
2526
2527 return template % qr_filename
2528
2529
2531 if data is None:
2532 return None
2533
2534
2535
2536 return data
2537
2538
2540 if data is None:
2541 return None
2542
2543 parts = data.split(self.__args_divider)
2544 if len(parts) < 3:
2545 return 'IF_NOT_EMPTY lacks <instead> definition'
2546 txt = parts[0]
2547 template = parts[1]
2548 instead = parts[2]
2549
2550 if txt.strip() == '':
2551 return instead
2552 if '%s' in template:
2553 return template % txt
2554 return template
2555
2556
2558
2559 if data is None:
2560 return None
2561 parts = data.split(self.__args_divider)
2562 if len(parts) < 2:
2563 return self._escape(u'IF_DEBUGGING lacks proper definition')
2564 debug_str = parts[0]
2565 non_debug_str = parts[1]
2566 if self.debug:
2567 return debug_str
2568 return non_debug_str
2569
2570
2571 - def _get_variant_free_text(self, data=None):
2572
2573 if data is None:
2574 parts = []
2575 msg = _('generic text')
2576 cache_key = 'free_text::%s' % datetime.datetime.now()
2577 else:
2578 parts = data.split(self.__args_divider)
2579 msg = parts[0]
2580 cache_key = 'free_text::%s' % msg
2581
2582 try:
2583 return self.__cache[cache_key]
2584 except KeyError:
2585 pass
2586
2587 if len(parts) > 1:
2588 preset = parts[1]
2589 else:
2590 preset = ''
2591
2592 dlg = gmGuiHelpers.cMultilineTextEntryDlg (
2593 None,
2594 -1,
2595 title = _('Replacing <free_text> placeholder'),
2596 msg = _('Below you can enter free text.\n\n [%s]') % msg,
2597 text = preset
2598 )
2599 dlg.enable_user_formatting = True
2600 decision = dlg.ShowModal()
2601 text = dlg.value.strip()
2602 is_user_formatted = dlg.is_user_formatted
2603 dlg.DestroyLater()
2604
2605 if decision != wx.ID_SAVE:
2606 if self.debug:
2607 return self._escape(_('Text input cancelled by user.'))
2608 return self._escape('')
2609
2610
2611 if is_user_formatted:
2612 self.__cache[cache_key] = text
2613 return text
2614
2615 text = self._escape(text)
2616 self.__cache[cache_key] = text
2617 return text
2618
2619
2640
2641
2643 try:
2644 bill = self.__cache['bill']
2645 except KeyError:
2646 from Gnumed.wxpython import gmBillingWidgets
2647 bill = gmBillingWidgets.manage_bills(patient = self.pat)
2648 if bill is None:
2649 if self.debug:
2650 return self._escape(_('no bill selected'))
2651 return ''
2652 self.__cache['bill'] = bill
2653
2654 format = 'qr'
2655 options = data.split(self.__args_divider)
2656 _log.debug('options: %s', options)
2657 for o in options:
2658 if o.strip().startswith('fmt='):
2659 format = o.strip()[4:]
2660 if format not in ['qr', 'txt']:
2661 return self._escape(_('praxis_scan2pay: invalid format (qr/txt)'))
2662 continue
2663 _log.debug('format: %s' % format)
2664
2665 from Gnumed.business import gmBilling
2666 data_str = gmBilling.get_scan2pay_data (
2667 gmPraxis.gmCurrentPraxisBranch(),
2668 bill,
2669 provider = gmStaff.gmCurrentProvider()
2670 )
2671 if data_str is None:
2672 if self.debug:
2673 return self._escape('bill_scan2pay-cannot_create_data_file')
2674 return ''
2675
2676 if format == 'txt':
2677 return self._escape(data_str)
2678
2679 if format == 'qr':
2680 qr_filename = gmTools.create_qrcode(text = data_str)
2681 if qr_filename is not None:
2682 return qr_filename
2683 if self.debug:
2684 return self._escape('bill_scan2pay-cannot_create_QR_code')
2685 return ''
2686
2687 return None
2688
2689
2710
2711
2713 try:
2714 bill = self.__cache['bill']
2715 except KeyError:
2716 from Gnumed.wxpython import gmBillingWidgets
2717 bill = gmBillingWidgets.manage_bills(patient = self.pat)
2718 if bill is None:
2719 if self.debug:
2720 return self._escape(_('no bill selected'))
2721 return ''
2722 self.__cache['bill'] = bill
2723 self.__cache['bill-adr'] = bill.address
2724
2725 try:
2726 bill_adr = self.__cache['bill-adr']
2727 except KeyError:
2728 bill_adr = bill.address
2729 self.__cache['bill-adr'] = bill_adr
2730
2731 if bill_adr is None:
2732 if self.debug:
2733 return self._escape(_('[%s] bill has no address') % part)
2734 return ''
2735
2736 if bill_adr[part] is None:
2737 return self._escape('')
2738
2739 if data is None:
2740 return self._escape(bill_adr[part])
2741
2742 if data == '':
2743 return self._escape(bill_adr[part])
2744
2745 return data % self._escape(bill_adr[part])
2746
2747
2749 return self.__get_variant_bill_adr_part(data = data, part = 'street')
2750
2751
2753 return self.__get_variant_bill_adr_part(data = data, part = 'number')
2754
2755
2757 return self.__get_variant_bill_adr_part(data = data, part = 'subunit')
2758
2760 return self.__get_variant_bill_adr_part(data = data, part = 'urb')
2761
2762
2764 return self.__get_variant_bill_adr_part(data = data, part = 'suburb')
2765
2766
2768 return self.__get_variant_bill_adr_part(data = data, part = 'postcode')
2769
2770
2772 return self.__get_variant_bill_adr_part(data = data, part = 'l10n_region')
2773
2774
2776 return self.__get_variant_bill_adr_part(data = data, part = 'l10n_country')
2777
2778
2779
2780
2782 if self.__esc_func is None:
2783 return text
2784 assert (text is not None), 'text=None passed to _escape()'
2785 return self.__esc_func(text)
2786
2787
2788 - def _escape_dict(self, the_dict=None, date_format='%Y %b %d %H:%M', none_string='', bool_strings=None):
2789 if bool_strings is None:
2790 bools = {True: _('true'), False: _('false')}
2791 else:
2792 bools = {True: bool_strings[0], False: bool_strings[1]}
2793 data = {}
2794 for field in the_dict.keys():
2795
2796
2797
2798
2799 val = the_dict[field]
2800 if val is None:
2801 data[field] = none_string
2802 continue
2803 if isinstance(val, bool):
2804 data[field] = bools[val]
2805 continue
2806 if isinstance(val, datetime.datetime):
2807 data[field] = gmDateTime.pydt_strftime(val, format = date_format)
2808 if self.__esc_style in ['latex', 'tex']:
2809 data[field] = gmTools.tex_escape_string(data[field])
2810 elif self.__esc_style in ['xetex', 'xelatex']:
2811 data[field] = gmTools.xetex_escape_string(data[field])
2812 continue
2813 try:
2814 data[field] = str(val, encoding = 'utf8', errors = 'replace')
2815 except TypeError:
2816 try:
2817 data[field] = str(val)
2818 except (UnicodeDecodeError, TypeError):
2819 val = '%s' % str(val)
2820 data[field] = val.decode('utf8', 'replace')
2821 if self.__esc_style in ['latex', 'tex']:
2822 data[field] = gmTools.tex_escape_string(data[field])
2823 elif self.__esc_style in ['xetex', 'xelatex']:
2824 data[field] = gmTools.xetex_escape_string(data[field])
2825 return data
2826
2827
2829
2830 _log.debug('testing for placeholders with pattern: %s', first_pass_placeholder_regex)
2831
2832 data_source = gmPlaceholderHandler()
2833 original_line = ''
2834
2835 while True:
2836
2837 line = wx.GetTextFromUser (
2838 _('Enter some text containing a placeholder:'),
2839 _('Testing placeholders'),
2840 centre = True,
2841 default_value = original_line
2842 )
2843 if line.strip() == '':
2844 break
2845 original_line = line
2846
2847 placeholders_in_line = regex.findall(first_pass_placeholder_regex, line, regex.IGNORECASE)
2848 if len(placeholders_in_line) == 0:
2849 continue
2850 for placeholder in placeholders_in_line:
2851 try:
2852 val = data_source[placeholder]
2853 except Exception:
2854 val = _('error with placeholder [%s]') % placeholder
2855 if val is None:
2856 val = _('error with placeholder [%s]') % placeholder
2857 line = line.replace(placeholder, val)
2858
2859 msg = _(
2860 'Input: %s\n'
2861 '\n'
2862 'Output:\n'
2863 '%s'
2864 ) % (
2865 original_line,
2866 line
2867 )
2868 gmGuiHelpers.gm_show_info (
2869 title = _('Testing placeholders'),
2870 info = msg
2871 )
2872
2873
2875 """Functions a macro can legally use.
2876
2877 An instance of this class is passed to the GNUmed scripting
2878 listener. Hence, all actions a macro can legally take must
2879 be defined in this class. Thus we achieve some screening for
2880 security and also thread safety handling.
2881 """
2882
2883 - def __init__(self, personality = None):
2884 if personality is None:
2885 raise gmExceptions.ConstructorError('must specify personality')
2886 self.__personality = personality
2887 self.__attached = 0
2888 self._get_source_personality = None
2889 self.__user_done = False
2890 self.__user_answer = 'no answer yet'
2891 self.__pat = gmPerson.gmCurrentPatient()
2892
2893 self.__auth_cookie = str(random.random())
2894 self.__pat_lock_cookie = str(random.random())
2895 self.__lock_after_load_cookie = str(random.random())
2896
2897 _log.info('slave mode personality is [%s]', personality)
2898
2899
2900
2901 - def attach(self, personality = None):
2902 if self.__attached:
2903 _log.error('attach with [%s] rejected, already serving a client', personality)
2904 return (0, _('attach rejected, already serving a client'))
2905 if personality != self.__personality:
2906 _log.error('rejecting attach to personality [%s], only servicing [%s]' % (personality, self.__personality))
2907 return (0, _('attach to personality [%s] rejected') % personality)
2908 self.__attached = 1
2909 self.__auth_cookie = str(random.random())
2910 return (1, self.__auth_cookie)
2911
2912 - def detach(self, auth_cookie=None):
2913 if not self.__attached:
2914 return 1
2915 if auth_cookie != self.__auth_cookie:
2916 _log.error('rejecting detach() with cookie [%s]' % auth_cookie)
2917 return 0
2918 self.__attached = 0
2919 return 1
2920
2922 if not self.__attached:
2923 return 1
2924 self.__user_done = False
2925
2926 wx.CallAfter(self._force_detach)
2927 return 1
2928
2930 ver = _cfg.get(option = 'client_version')
2931 return "GNUmed %s, %s $Revision: 1.51 $" % (ver, self.__class__.__name__)
2932
2934 """Shuts down this client instance."""
2935 if not self.__attached:
2936 return 0
2937 if auth_cookie != self.__auth_cookie:
2938 _log.error('non-authenticated shutdown_gnumed()')
2939 return 0
2940 wx.CallAfter(self._shutdown_gnumed, forced)
2941 return 1
2942
2944 """Raise ourselves to the top of the desktop."""
2945 if not self.__attached:
2946 return 0
2947 if auth_cookie != self.__auth_cookie:
2948 _log.error('non-authenticated raise_gnumed()')
2949 return 0
2950 return "cMacroPrimitives.raise_gnumed() not implemented"
2951
2953 if not self.__attached:
2954 return 0
2955 if auth_cookie != self.__auth_cookie:
2956 _log.error('non-authenticated get_loaded_plugins()')
2957 return 0
2958 gb = gmGuiBroker.GuiBroker()
2959 return gb['horstspace.notebook.gui'].keys()
2960
2962 """Raise a notebook plugin within GNUmed."""
2963 if not self.__attached:
2964 return 0
2965 if auth_cookie != self.__auth_cookie:
2966 _log.error('non-authenticated raise_notebook_plugin()')
2967 return 0
2968
2969 wx.CallAfter(gmPlugin.raise_notebook_plugin, a_plugin)
2970 return 1
2971
2973 """Load external patient, perhaps create it.
2974
2975 Callers must use get_user_answer() to get status information.
2976 It is unsafe to proceed without knowing the completion state as
2977 the controlled client may be waiting for user input from a
2978 patient selection list.
2979 """
2980 if not self.__attached:
2981 return (0, _('request rejected, you are not attach()ed'))
2982 if auth_cookie != self.__auth_cookie:
2983 _log.error('non-authenticated load_patient_from_external_source()')
2984 return (0, _('rejected load_patient_from_external_source(), not authenticated'))
2985 if self.__pat.locked:
2986 _log.error('patient is locked, cannot load from external source')
2987 return (0, _('current patient is locked'))
2988 self.__user_done = False
2989 wx.CallAfter(self._load_patient_from_external_source)
2990 self.__lock_after_load_cookie = str(random.random())
2991 return (1, self.__lock_after_load_cookie)
2992
2994 if not self.__attached:
2995 return (0, _('request rejected, you are not attach()ed'))
2996 if auth_cookie != self.__auth_cookie:
2997 _log.error('non-authenticated lock_load_patient()')
2998 return (0, _('rejected lock_load_patient(), not authenticated'))
2999
3000 if lock_after_load_cookie != self.__lock_after_load_cookie:
3001 _log.warning('patient lock-after-load request rejected due to wrong cookie [%s]' % lock_after_load_cookie)
3002 return (0, 'patient lock-after-load request rejected, wrong cookie provided')
3003 self.__pat.locked = True
3004 self.__pat_lock_cookie = str(random.random())
3005 return (1, self.__pat_lock_cookie)
3006
3008 if not self.__attached:
3009 return (0, _('request rejected, you are not attach()ed'))
3010 if auth_cookie != self.__auth_cookie:
3011 _log.error('non-authenticated lock_into_patient()')
3012 return (0, _('rejected lock_into_patient(), not authenticated'))
3013 if self.__pat.locked:
3014 _log.error('patient is already locked')
3015 return (0, _('already locked into a patient'))
3016 searcher = gmPersonSearch.cPatientSearcher_SQL()
3017 if type(search_params) == dict:
3018 idents = searcher.get_identities(search_dict=search_params)
3019 raise Exception("must use dto, not search_dict")
3020 else:
3021 idents = searcher.get_identities(search_term=search_params)
3022 if idents is None:
3023 return (0, _('error searching for patient with [%s]/%s') % (search_term, search_dict))
3024 if len(idents) == 0:
3025 return (0, _('no patient found for [%s]/%s') % (search_term, search_dict))
3026
3027 if len(idents) > 1:
3028 return (0, _('several matching patients found for [%s]/%s') % (search_term, search_dict))
3029 if not gmPatSearchWidgets.set_active_patient(patient = idents[0]):
3030 return (0, _('cannot activate patient [%s] (%s/%s)') % (str(idents[0]), search_term, search_dict))
3031 self.__pat.locked = True
3032 self.__pat_lock_cookie = str(random.random())
3033 return (1, self.__pat_lock_cookie)
3034
3036 if not self.__attached:
3037 return (0, _('request rejected, you are not attach()ed'))
3038 if auth_cookie != self.__auth_cookie:
3039 _log.error('non-authenticated unlock_patient()')
3040 return (0, _('rejected unlock_patient, not authenticated'))
3041
3042 if not self.__pat.locked:
3043 return (1, '')
3044
3045 if unlock_cookie != self.__pat_lock_cookie:
3046 _log.warning('patient unlock request rejected due to wrong cookie [%s]' % unlock_cookie)
3047 return (0, 'patient unlock request rejected, wrong cookie provided')
3048 self.__pat.locked = False
3049 return (1, '')
3050
3052 if not self.__attached:
3053 return 0
3054 if auth_cookie != self.__auth_cookie:
3055 _log.error('non-authenticated select_identity()')
3056 return 0
3057 return "cMacroPrimitives.assume_staff_identity() not implemented"
3058
3060 if not self.__user_done:
3061 return (0, 'still waiting')
3062 self.__user_done = False
3063 return (1, self.__user_answer)
3064
3065
3066
3068 msg = _(
3069 'Someone tries to forcibly break the existing\n'
3070 'controlling connection. This may or may not\n'
3071 'have legitimate reasons.\n\n'
3072 'Do you want to allow breaking the connection ?'
3073 )
3074 can_break_conn = gmGuiHelpers.gm_show_question (
3075 aMessage = msg,
3076 aTitle = _('forced detach attempt')
3077 )
3078 if can_break_conn:
3079 self.__user_answer = 1
3080 else:
3081 self.__user_answer = 0
3082 self.__user_done = True
3083 if can_break_conn:
3084 self.__pat.locked = False
3085 self.__attached = 0
3086 return 1
3087
3089 top_win = wx.GetApp().GetTopWindow()
3090 if forced:
3091 top_win.DestroyLater()
3092 else:
3093 top_win.Close()
3094
3103
3104
3105
3106 if __name__ == '__main__':
3107
3108 if len(sys.argv) < 2:
3109 sys.exit()
3110
3111 if sys.argv[1] != 'test':
3112 sys.exit()
3113
3114 gmI18N.activate_locale()
3115 gmI18N.install_domain()
3116
3117
3119 handler = gmPlaceholderHandler()
3120 handler.debug = True
3121
3122 for placeholder in ['a', 'b']:
3123 print(handler[placeholder])
3124
3125 pat = gmPersonSearch.ask_for_patient()
3126 if pat is None:
3127 return
3128
3129 gmPatSearchWidgets.set_active_patient(patient = pat)
3130
3131 print('DOB (YYYY-MM-DD):', handler['date_of_birth::%Y-%m-%d'])
3132
3133 app = wx.PyWidgetTester(size = (200, 50))
3134
3135 ph = 'progress_notes::ap'
3136 print('%s: %s' % (ph, handler[ph]))
3137
3139
3140 tests = [
3141
3142 '$<lastname>$',
3143 '$<lastname::::3>$',
3144 '$<name::%(title)s %(firstnames)s%(preferred)s%(lastnames)s>$',
3145
3146
3147 'lastname',
3148 '$<lastname',
3149 '$<lastname::',
3150 '$<lastname::>$',
3151 '$<lastname::abc>$',
3152 '$<lastname::abc::>$',
3153 '$<lastname::abc::3>$',
3154 '$<lastname::abc::xyz>$',
3155 '$<lastname::::>$',
3156 '$<lastname::::xyz>$',
3157
3158 '$<date_of_birth::%Y-%m-%d>$',
3159 '$<date_of_birth::%Y-%m-%d::3>$',
3160 '$<date_of_birth::%Y-%m-%d::>$',
3161
3162
3163 '$<adr_location::home::35>$',
3164 '$<gender_mapper::male//female//other::5>$',
3165 '$<current_meds::==> %(product)s %(preparation)s (%(substance)s) <==\n::50>$',
3166 '$<allergy_list::%(descriptor)s, >$',
3167 '$<current_meds_table::latex//>$'
3168
3169
3170
3171
3172
3173
3174
3175
3176
3177
3178
3179
3180
3181
3182 ]
3183
3184
3185
3186
3187
3188 pat = gmPersonSearch.ask_for_patient()
3189 if pat is None:
3190 return
3191
3192 gmPatSearchWidgets.set_active_patient(patient = pat)
3193
3194 handler = gmPlaceholderHandler()
3195 handler.debug = True
3196
3197 for placeholder in tests:
3198 print(placeholder, "=>", handler[placeholder])
3199 print("--------------")
3200 input()
3201
3202
3203
3204
3205
3206
3207
3208
3209
3211 from Gnumed.pycommon import gmScriptingListener
3212 import xmlrpc.client
3213
3214 listener = gmScriptingListener.cScriptingListener(macro_executor = cMacroPrimitives(personality='unit test'), port=9999)
3215
3216 s = xmlrpc.client.ServerProxy('http://localhost:9999')
3217 print("should fail:", s.attach())
3218 print("should fail:", s.attach('wrong cookie'))
3219 print("should work:", s.version())
3220 print("should fail:", s.raise_gnumed())
3221 print("should fail:", s.raise_notebook_plugin('test plugin'))
3222 print("should fail:", s.lock_into_patient('kirk, james'))
3223 print("should fail:", s.unlock_patient())
3224 status, conn_auth = s.attach('unit test')
3225 print("should work:", status, conn_auth)
3226 print("should work:", s.version())
3227 print("should work:", s.raise_gnumed(conn_auth))
3228 status, pat_auth = s.lock_into_patient(conn_auth, 'kirk, james')
3229 print("should work:", status, pat_auth)
3230 print("should fail:", s.unlock_patient(conn_auth, 'bogus patient unlock cookie'))
3231 print("should work", s.unlock_patient(conn_auth, pat_auth))
3232 data = {'firstname': 'jame', 'lastnames': 'Kirk', 'gender': 'm'}
3233 status, pat_auth = s.lock_into_patient(conn_auth, data)
3234 print("should work:", status, pat_auth)
3235 print("should work", s.unlock_patient(conn_auth, pat_auth))
3236 print(s.detach('bogus detach cookie'))
3237 print(s.detach(conn_auth))
3238 del s
3239
3240 listener.shutdown()
3241
3243
3244 import re as regex
3245
3246 tests = [
3247 ' $<lastname>$ ',
3248 ' $<lastname::::3>$ ',
3249
3250
3251 '$<date_of_birth::%Y-%m-%d>$',
3252 '$<date_of_birth::%Y-%m-%d::3>$',
3253 '$<date_of_birth::%Y-%m-%d::>$',
3254
3255 '$<adr_location::home::35>$',
3256 '$<gender_mapper::male//female//other::5>$',
3257 '$<current_meds::==> %(product)s %(preparation)s (%(substance)s) <==\\n::50>$',
3258 '$<allergy_list::%(descriptor)s, >$',
3259
3260 '\\noindent Patient: $<lastname>$, $<firstname>$',
3261 '$<allergies::%(descriptor)s & %(l10n_type)s & {\\footnotesize %(reaction)s} \tabularnewline \hline >$',
3262 '$<current_meds:: \item[%(substance)s] {\\footnotesize (%(product)s)} %(preparation)s %(amount)s%(unit)s: %(schedule)s >$'
3263 ]
3264
3265 tests = [
3266
3267 'junk $<lastname::::3>$ junk',
3268 'junk $<lastname::abc::3>$ junk',
3269 'junk $<lastname::abc>$ junk',
3270 'junk $<lastname>$ junk',
3271
3272 'junk $<lastname>$ junk $<firstname>$ junk',
3273 'junk $<lastname::abc>$ junk $<fiststname::abc>$ junk',
3274 'junk $<lastname::abc::3>$ junk $<firstname::abc::3>$ junk',
3275 'junk $<lastname::::3>$ junk $<firstname::::3>$ junk'
3276
3277 ]
3278
3279 tests = [
3280
3281
3282
3283
3284
3285 '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',
3286 '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',
3287 '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',
3288
3289 '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',
3290 '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',
3291 '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',
3292 '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',
3293 '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',
3294 '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',
3295 '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',
3296 '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',
3297 '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',
3298 ]
3299
3300 tests = [
3301 'junk $<<<should pass::template::>>>$ junk',
3302 'junk $<<<should pass::template::10>>>$ junk',
3303 'junk $<<<should pass::template::10-20>>>$ junk',
3304 'junk $<<<should pass::template $<<dummy::template 2::10>>$::>>>$ junk',
3305 'junk $<<<should pass::template $<dummy::template 2::10>$::>>>$ junk',
3306
3307 'junk $<<<should pass::template::>>>$ junk $<<<should pass 2::template 2::>>>$ junk',
3308 'junk $<<<should pass::template::>>>$ junk $<<should pass 2::template 2::>>$ junk',
3309 'junk $<<<should pass::template::>>>$ junk $<should pass 2::template 2::>$ junk',
3310
3311 'junk $<<<should fail::template $<<<dummy::template 2::10>>>$::>>>$ junk',
3312
3313 'junk $<<<should fail::template::10->>>$ junk',
3314 'junk $<<<should fail::template::10->>>$ junk',
3315 'junk $<<<should fail::template::10->>>$ junk',
3316 'junk $<<<should fail::template::10->>>$ junk',
3317 'junk $<first_pass::junk $<<<3rd_pass::template::20>>>$ junk::8-10>$ junk'
3318 ]
3319
3320
3321
3322
3323
3324
3325
3326
3327
3328
3329
3330
3331
3332
3333
3334 all_tests = {
3335 first_pass_placeholder_regex: [
3336
3337 ('junk $<first_level::template::>$ junk', ['$<first_level::template::>$']),
3338 ('junk $<first_level::template::10>$ junk', ['$<first_level::template::10>$']),
3339 ('junk $<first_level::template::10-12>$ junk', ['$<first_level::template::10-12>$']),
3340
3341
3342 ('junk $<first_level::$<<insert::insert_template::0>>$::10-12>$ junk', ['$<first_level::$<<insert::insert_template::0>>$::10-12>$']),
3343 ('junk $<first_level::$<<<insert::insert_template::0>>>$::10-12>$ junk', ['$<first_level::$<<<insert::insert_template::0>>>$::10-12>$']),
3344
3345
3346 ('junk $<<second_level::$<insert::insert_template::0>$::10-12>>$ junk', ['$<insert::insert_template::0>$']),
3347 ('junk $<<<third_level::$<insert::insert_template::0>$::10-12>>>$ junk', ['$<insert::insert_template::0>$']),
3348
3349
3350 ('junk $<first_level 1::template 1::>$ junk $<<second_level 2::template 2::>>$ junk', ['$<first_level 1::template 1::>$']),
3351 ('junk $<first_level 1::template 1::>$ junk $<<<third_level 2::template 2::>>>$ junk', ['$<first_level 1::template 1::>$']),
3352
3353
3354 ('junk $<first_level 1::template 1::>$ junk $<first_level 2::template 2::>$ junk', ['$<first_level 1::template 1::>$', '$<first_level 2::template 2::>$']),
3355
3356
3357 ('returns illegal match: junk $<first_level::$<insert::insert_template::0>$::10-12>$ junk', ['$<first_level::$<insert::insert_template::0>$::10-12>$']),
3358 ],
3359 second_pass_placeholder_regex: [
3360
3361 ('junk $<<second_level::template::>>$ junk', ['$<<second_level::template::>>$']),
3362 ('junk $<<second_level::template::10>>$ junk', ['$<<second_level::template::10>>$']),
3363 ('junk $<<second_level::template::10-12>>$ junk', ['$<<second_level::template::10-12>>$']),
3364
3365
3366 ('junk $<<second_level::$<insert::insert_template::0>$::10-12>>$ junk', ['$<<second_level::$<insert::insert_template::0>$::10-12>>$']),
3367 ('junk $<<second_level::$<<<insert::insert_template::0>>>$::10-12>>$ junk', ['$<<second_level::$<<<insert::insert_template::0>>>$::10-12>>$']),
3368
3369
3370 ('junk $<first_level::$<<insert::insert_template::0>>$::10-12>$ junk', ['$<<insert::insert_template::0>>$']),
3371 ('junk $<<<third_level::$<<insert::insert_template::0>>$::10-12>>>$ junk', ['$<<insert::insert_template::0>>$']),
3372
3373
3374 ('junk $<first_level 1::template 1::>$ junk $<<second_level 2::template 2::>>$ junk', ['$<<second_level 2::template 2::>>$']),
3375 ('junk $<<second_level 1::template 1::>>$ junk $<<<third_level 2::template 2::>>>$ junk', ['$<<second_level 1::template 1::>>$']),
3376
3377
3378 ('junk $<<second_level 1::template 1::>>$ junk $<<second_level 2::template 2::>>$ junk', ['$<<second_level 1::template 1::>>$', '$<<second_level 2::template 2::>>$']),
3379
3380
3381 ('returns illegal match: junk $<<second_level::$<<insert::insert_template::0>>$::10-12>>$ junk', ['$<<second_level::$<<insert::insert_template::0>>$::10-12>>$']),
3382
3383 ],
3384 third_pass_placeholder_regex: [
3385
3386 ('junk $<<<third_level::template::>>>$ junk', ['$<<<third_level::template::>>>$']),
3387 ('junk $<<<third_level::template::10>>>$ junk', ['$<<<third_level::template::10>>>$']),
3388 ('junk $<<<third_level::template::10-12>>>$ junk', ['$<<<third_level::template::10-12>>>$']),
3389
3390
3391 ('junk $<<<third_level::$<<insert::insert_template::0>>$::10-12>>>$ junk', ['$<<<third_level::$<<insert::insert_template::0>>$::10-12>>>$']),
3392 ('junk $<<<third_level::$<insert::insert_template::0>$::10-12>>>$ junk', ['$<<<third_level::$<insert::insert_template::0>$::10-12>>>$']),
3393
3394
3395 ('junk $<<second_level::$<<<insert::insert_template::0>>>$::10-12>>$ junk', ['$<<<insert::insert_template::0>>>$']),
3396 ('junk $<first_level::$<<<insert::insert_template::0>>>$::10-12>$ junk', ['$<<<insert::insert_template::0>>>$']),
3397
3398
3399 ('junk $<first_level 1::template 1::>$ junk $<<<third_level 2::template 2::>>>$ junk', ['$<<<third_level 2::template 2::>>>$']),
3400 ('junk $<<second_level 1::template 1::>>$ junk $<<<third_level 2::template 2::>>>$ junk', ['$<<<third_level 2::template 2::>>>$']),
3401
3402
3403 ('returns illegal match: junk $<<<third_level::$<<<insert::insert_template::0>>>$::10-12>>>$ junk', ['$<<<third_level::$<<<insert::insert_template::0>>>$::10-12>>>$']),
3404 ]
3405 }
3406
3407 for pattern in [first_pass_placeholder_regex, second_pass_placeholder_regex, third_pass_placeholder_regex]:
3408 print("")
3409 print("-----------------------------")
3410 print("regex:", pattern)
3411 tests = all_tests[pattern]
3412 for t in tests:
3413 line, expected_results = t
3414 phs = regex.findall(pattern, line, regex.IGNORECASE)
3415 if len(phs) > 0:
3416 if phs == expected_results:
3417 continue
3418
3419 print("")
3420 print("failed")
3421 print("line:", line)
3422
3423 if len(phs) == 0:
3424 print("no match")
3425 continue
3426
3427 if len(phs) > 1:
3428 print("several matches")
3429 for r in expected_results:
3430 print("expected:", r)
3431 for p in phs:
3432 print("found:", p)
3433 continue
3434
3435 print("unexpected match")
3436 print("expected:", expected_results)
3437 print("found: ", phs)
3438
3439
3441
3442 phs = [
3443
3444
3445
3446
3447
3448
3449
3450
3451
3452
3453
3454
3455
3456
3457
3458
3459
3460
3461
3462
3463
3464
3465
3466
3467
3468
3469
3470
3471
3472
3473
3474
3475
3476
3477
3478
3479
3480
3481
3482
3483
3484
3485
3486
3487
3488
3489
3490
3491
3492
3493
3494
3495
3496
3497
3498
3499
3500
3501
3502
3503
3504
3505
3506
3507
3508
3509
3510
3511
3512
3513
3514 u'$<bill_scan2pay::fmt=txt::>$',
3515 u'$<bill_scan2pay::fmt=qr::>$'
3516 ]
3517
3518 handler = gmPlaceholderHandler()
3519 handler.debug = True
3520
3521 gmStaff.set_current_provider_to_logged_on_user()
3522 gmPraxisWidgets.set_active_praxis_branch(no_parent = True)
3523 pat = gmPersonSearch.ask_for_patient()
3524 if pat is None:
3525 return
3526 gmPatSearchWidgets.set_active_patient(patient = pat)
3527
3528
3529
3530 for ph in phs:
3531 print(ph)
3532 print(" result:")
3533 print(' %s' % handler[ph])
3534
3535
3536
3544
3545
3548
3549
3550
3551 app = wx.App()
3552
3553
3554
3555
3556
3557
3558 test_placeholder()
3559
3560