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