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