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 codecs
16
17
18 import wx
19
20
21 if __name__ == '__main__':
22 sys.path.insert(0, '../../')
23 from Gnumed.pycommon import gmI18N
24 if __name__ == '__main__':
25 gmI18N.activate_locale()
26 gmI18N.install_domain()
27 from Gnumed.pycommon import gmGuiBroker
28 from Gnumed.pycommon import gmTools
29 from Gnumed.pycommon import gmBorg
30 from Gnumed.pycommon import gmExceptions
31 from Gnumed.pycommon import gmCfg2
32 from Gnumed.pycommon import gmDateTime
33 from Gnumed.pycommon import gmMimeLib
34
35 from Gnumed.business import gmPerson
36 from Gnumed.business import gmStaff
37 from Gnumed.business import gmDemographicRecord
38 from Gnumed.business import gmMedication
39 from Gnumed.business import gmPathLab
40 from Gnumed.business import gmPersonSearch
41 from Gnumed.business import gmVaccination
42 from Gnumed.business import gmKeywordExpansion
43 from Gnumed.business import gmPraxis
44
45 from Gnumed.wxpython import gmGuiHelpers
46 from Gnumed.wxpython import gmNarrativeWidgets
47 from Gnumed.wxpython import gmPatSearchWidgets
48 from Gnumed.wxpython import gmPersonContactWidgets
49 from Gnumed.wxpython import gmPlugin
50 from Gnumed.wxpython import gmEMRStructWidgets
51 from Gnumed.wxpython import gmListWidgets
52 from Gnumed.wxpython import gmDemographicsWidgets
53 from Gnumed.wxpython import gmDocumentWidgets
54 from Gnumed.wxpython import gmKeywordExpansionWidgets
55 from Gnumed.wxpython import gmMeasurementWidgets
56 from Gnumed.wxpython import gmPraxisWidgets
57
58
59 _log = logging.getLogger('gm.scripting')
60 _cfg = gmCfg2.gmCfgData()
61
62
63
64
65
66 _injectable_placeholders = {
67 u'form_name_long': None,
68 u'form_name_short': None,
69 u'form_version': None
70 }
71
72
73
74 known_variant_placeholders = [
75
76 u'free_text',
77
78 u'text_snippet',
79
80 u'data_snippet',
81
82
83
84
85
86
87 u'tex_escape',
88 u'today',
89 u'gender_mapper',
90
91
92 u'client_version',
93
94
95 u'name',
96 u'date_of_birth',
97
98 u'patient_address',
99 u'adr_street',
100 u'adr_number',
101 u'adr_subunit',
102 u'adr_location',
103 u'adr_suburb',
104 u'adr_postcode',
105 u'adr_region',
106 u'adr_country',
107
108 u'patient_comm',
109 u'patient_tags',
110
111
112 u'patient_photo',
113
114
115
116
117
118
119
120 u'external_id',
121
122
123
124 u'soap',
125 u'soap_s',
126 u'soap_o',
127 u'soap_a',
128 u'soap_p',
129 u'soap_u',
130 u'soap_admin',
131 u'progress_notes',
132
133
134
135 u'soap_for_encounters',
136
137
138
139 u'soap_by_issue',
140
141
142
143 u'soap_by_episode',
144
145
146
147 u'emr_journal',
148
149
150
151
152
153
154
155
156
157 u'current_meds',
158
159 u'current_meds_for_rx',
160
161
162
163
164
165
166 u'current_meds_table',
167 u'current_meds_notes',
168
169 u'lab_table',
170 u'test_results',
171
172 u'latest_vaccs_table',
173 u'vaccination_history',
174
175 u'allergy_state',
176 u'allergies',
177 u'allergy_list',
178 u'problems',
179 u'PHX',
180 u'encounter_list',
181
182 u'documents',
183
184
185
186
187
188
189
190
191
192
193
194
195
196 u'reminders',
197
198
199
200
201 u'current_provider',
202 u'current_provider_external_id',
203 u'primary_praxis_provider',
204 u'primary_praxis_provider_external_id',
205
206
207 u'praxis',
208
209
210
211
212 u'praxis_address',
213
214
215 u'bill',
216 u'bill_item'
217 ]
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232 default_placeholder_regex = r'\$<[^<:]+::.*?::\d*?>\$'
233 first_order_placeholder_regex = r'\$<<<[^<:]+?::.*::\d*?>>>\$'
234 second_order_placeholder_regex = r'\$<<[^<:]+?::.*::\d*?>>\$'
235 third_order_placeholder_regex = r'\$<[^<:]+::.*?::\d*?>\$'
236
237
238
239
240 default_placeholder_start = u'$<'
241 default_placeholder_end = u'>$'
242
244 fname = gmTools.get_unique_filename(prefix = 'gm-placeholders-', suffix = '.txt')
245 ph_file = codecs.open(filename = fname, mode = 'wb', encoding = 'utf8', errors = 'replace')
246
247 ph_file.write(u'Here you can find some more documentation on placeholder use:\n')
248 ph_file.write(u'\n http://wiki.gnumed.de/bin/view/Gnumed/GmManualLettersForms\n\n\n')
249
250 ph_file.write(u'Variable placeholders (use like: $<PLACEHOLDER_NAME::ARGUMENTS::MAX OUTPUT LENGTH>$):\n')
251 for ph in known_variant_placeholders:
252 ph_file.write(u' %s\n' % ph)
253 ph_file.write(u'\n')
254
255 ph_file.write(u'Injectable placeholders (use like: $<PLACEHOLDER_NAME::ARGUMENTS::MAX OUTPUT LENGTH>$):\n')
256 for ph in _injectable_placeholders:
257 ph_file.write(u' %s\n' % ph)
258 ph_file.write(u'\n')
259
260 ph_file.close()
261 gmMimeLib.call_viewer_on_file(aFile = fname, block = False)
262
264 """Returns values for placeholders.
265
266 - patient related placeholders operate on the currently active patient
267 - is passed to the forms handling code, for example
268
269 Return values when .debug is False:
270 - errors with placeholders return None
271 - placeholders failing to resolve to a value return an empty string
272
273 Return values when .debug is True:
274 - errors with placeholders return an error string
275 - placeholders failing to resolve to a value return a warning string
276
277 There are several types of placeholders:
278
279 injectable placeholders
280 - they must be set up before use by set_placeholder()
281 - they should be removed after use by unset_placeholder()
282 - the syntax is like extended static placeholders
283 - they are listed in _injectable_placeholders
284
285 variant placeholders
286 - those are listed in known_variant_placeholders
287 - they are parsed into placeholder, data, and maximum length
288 - the length is optional
289 - data is passed to the handler
290
291 Note that this cannot be called from a non-gui thread unless
292 wrapped in wx.CallAfter().
293 """
295
296 self.pat = gmPerson.gmCurrentPatient()
297 self.debug = False
298
299 self.invalid_placeholder_template = _('invalid placeholder >>>>>%s<<<<<')
300
301 self.__cache = {}
302
303 self.__esc_style = None
304 self.__esc_func = lambda x:x
305
306
307
311
315
317 self.__cache[key] = value
318
320 del self.__cache[key]
321
325
326 escape_style = property(lambda x:x, _set_escape_style)
327
336
337 escape_function = property(lambda x:x, _set_escape_function)
338
339 placeholder_regex = property(lambda x: default_placeholder_regex, lambda x:x)
340
341 first_order_placeholder_regex = property(lambda x: first_order_placeholder_regex, lambda x:x)
342 second_order_placeholder_regex = property(lambda x: second_order_placeholder_regex, lambda x:x)
343 third_order_placeholder_regex = property(lambda x: third_order_placeholder_regex, lambda x:x)
344
345
346
348 """Map self['placeholder'] to self.placeholder.
349
350 This is useful for replacing placeholders parsed out
351 of documents as strings.
352
353 Unknown/invalid placeholders still deliver a result but
354 it will be glaringly obvious if debugging is enabled.
355 """
356 _log.debug('replacing [%s]', placeholder)
357
358 original_placeholder = placeholder
359
360
361 if placeholder.startswith(default_placeholder_start):
362 placeholder = placeholder.lstrip('$').lstrip('<')
363 if placeholder.endswith(default_placeholder_end):
364 placeholder = placeholder.rstrip('$').rstrip('>')
365 else:
366 _log.error('placeholder must either start with [%s] and end with [%s] or neither of both', default_placeholder_start, default_placeholder_end)
367 if self.debug:
368 return self._escape(self.invalid_placeholder_template % original_placeholder)
369 return None
370
371
372 parts = placeholder.split('::::', 1)
373 if len(parts) == 2:
374 name, lng = parts
375 is_an_injectable = True
376 try:
377 val = _injectable_placeholders[name]
378 except KeyError:
379 is_an_injectable = False
380 except:
381 _log.exception('injectable placeholder handling error: %s', original_placeholder)
382 if self.debug:
383 return self._escape(self.invalid_placeholder_template % original_placeholder)
384 return None
385 if is_an_injectable:
386 if val is None:
387 if self.debug:
388 return self._escape(u'injectable placeholder [%s]: no value available' % name)
389 return placeholder
390 try:
391 lng = int(lng)
392 except (TypeError, ValueError):
393 lng = len(val)
394 return val[:lng]
395
396
397 if len(placeholder.split('::', 2)) < 3:
398 _log.error('invalid placeholder structure: %s', original_placeholder)
399 if self.debug:
400 return self._escape(self.invalid_placeholder_template % original_placeholder)
401 return None
402
403 name, data = placeholder.split('::', 1)
404 data, lng_str = data.rsplit('::', 1)
405 _log.debug('placeholder parts: name=[%s]; length=[%s]; options=>>>%s<<<', name, lng_str, data)
406 try:
407 lng = int(lng_str)
408 except (TypeError, ValueError):
409 lng = None
410
411 handler = getattr(self, '_get_variant_%s' % name, None)
412 if handler is None:
413 _log.warning('no handler <_get_variant_%s> for placeholder %s', name, original_placeholder)
414 if self.debug:
415 return self._escape(self.invalid_placeholder_template % original_placeholder)
416 return None
417
418 try:
419 if lng is None:
420 return handler(data = data)
421 return handler(data = data)[:lng]
422 except:
423 _log.exception('placeholder handling error: %s', original_placeholder)
424 if self.debug:
425 return self._escape(self.invalid_placeholder_template % original_placeholder)
426 return None
427
428 _log.error('something went wrong, should never get here')
429 return None
430
431
432
434 return self._escape (
435 gmTools.coalesce (
436 _cfg.get(option = u'client_version'),
437 u'%s' % self.__class__.__name__
438 )
439 )
440
442
443 from Gnumed.wxpython import gmProviderInboxWidgets
444
445 template = _('due %(due_date)s: %(comment)s (%(interval_due)s)')
446 date_format = '%Y %b %d'
447
448 data_parts = data.split('//')
449
450 if len(data_parts) > 0:
451 if data_parts[0].strip() != u'':
452 template = data_parts[0]
453
454 if len(data_parts) > 1:
455 if data_parts[1].strip() != u'':
456 date_format = data_parts[1]
457
458 reminders = gmProviderInboxWidgets.manage_reminders(patient = self.pat.ID)
459
460 if reminders is None:
461 return u''
462
463 if len(reminders) == 0:
464 return u''
465
466 lines = [ template % r.fields_as_dict(date_format = date_format, escape_style = self.__esc_style) for r in reminders ]
467
468 return u'\n'.join(lines)
469
471
472 select = False
473 include_descriptions = False
474 template = u'%s'
475 path_template = None
476 export_path = None
477
478 data_parts = data.split('//')
479
480 if u'select' in data_parts:
481 select = True
482 data_parts.remove(u'select')
483
484 if u'description' in data_parts:
485 include_descriptions = True
486 data_parts.remove(u'description')
487
488 template = data_parts[0]
489
490 if len(data_parts) > 1:
491 path_template = data_parts[1]
492
493 if len(data_parts) > 2:
494 export_path = data_parts[2]
495
496
497 if export_path is not None:
498 export_path = os.path.normcase(os.path.expanduser(export_path))
499 gmTools.mkdir(export_path)
500
501
502 if select:
503 docs = gmDocumentWidgets.manage_documents(msg = _('Select the patient documents to reference from the new document.'), single_selection = False)
504 else:
505 docs = self.pat.document_folder.documents
506
507 if docs is None:
508 return u''
509
510 lines = []
511 for doc in docs:
512 lines.append(template % doc.fields_as_dict(date_format = '%Y %b %d', escape_style = self.__esc_style))
513 if include_descriptions:
514 for desc in doc.get_descriptions(max_lng = None):
515 lines.append(self._escape(desc['text'] + u'\n'))
516 if path_template is not None:
517 for part_name in doc.export_parts_to_files(export_dir = export_path):
518 path, name = os.path.split(part_name)
519 lines.append(path_template % {'fullpath': part_name, 'name': name})
520
521 return u'\n'.join(lines)
522
524
525 encounters = gmEMRStructWidgets.select_encounters(single_selection = False)
526 if not encounters:
527 return u''
528
529 template = data
530
531 lines = []
532 for enc in encounters:
533 try:
534 lines.append(template % enc.fields_as_dict(date_format = '%Y %b %d', escape_style = self.__esc_style))
535 except:
536 lines.append(u'error formatting encounter')
537 _log.exception('problem formatting encounter list')
538 _log.error('template: %s', template)
539 _log.error('encounter: %s', encounter)
540
541 return u'\n'.join(lines)
542
544 """Select encounters from list and format SOAP thereof.
545
546 data: soap_cats (' ' -> None -> admin) // date format
547 """
548
549 cats = None
550 date_format = None
551
552 if data is not None:
553 data_parts = data.split('//')
554
555
556 if len(data_parts[0]) > 0:
557 cats = []
558 if u' ' in data_parts[0]:
559 cats.append(None)
560 data_parts[0] = data_parts[0].replace(u' ', u'')
561 cats.extend(list(data_parts[0]))
562
563
564 if len(data_parts) > 1:
565 if len(data_parts[1]) > 0:
566 date_format = data_parts[1]
567
568 encounters = gmEMRStructWidgets.select_encounters(single_selection = False)
569 if not encounters:
570 return u''
571
572 chunks = []
573 for enc in encounters:
574 chunks.append(enc.format_latex (
575 date_format = date_format,
576 soap_cats = cats,
577 soap_order = u'soap_rank, date'
578 ))
579
580 return u''.join(chunks)
581
583
584 cats = list(u'soapu')
585 cats.append(None)
586 template = u'%s'
587 interactive = True
588 line_length = 9999
589 target_format = None
590 time_range = None
591
592 if data is not None:
593 data_parts = data.split('//')
594
595
596 cats = []
597
598 for c in list(data_parts[0]):
599 if c == u' ':
600 c = None
601 cats.append(c)
602
603 if cats == u'':
604 cats = list(u'soapu').append(None)
605
606
607 if len(data_parts) > 1:
608 template = data_parts[1]
609
610
611 if len(data_parts) > 2:
612 try:
613 line_length = int(data_parts[2])
614 except:
615 line_length = 9999
616
617
618 if len(data_parts) > 3:
619 try:
620 time_range = 7 * int(data_parts[3])
621 except:
622
623
624 time_range = data_parts[3]
625
626
627 if len(data_parts) > 4:
628 target_format = data_parts[4]
629
630
631 narr = self.pat.emr.get_as_journal(soap_cats = cats, time_range = time_range)
632
633 if len(narr) == 0:
634 return u''
635
636 keys = narr[0].keys()
637 lines = []
638 line_dict = {}
639 for n in narr:
640 for key in keys:
641 if isinstance(n[key], basestring):
642 line_dict[key] = self._escape(text = n[key])
643 continue
644 line_dict[key] = n[key]
645 try:
646 lines.append((template % line_dict)[:line_length])
647 except KeyError:
648 return u'invalid key in template [%s], valid keys: %s]' % (template, str(keys))
649
650 return u'\n'.join(lines)
651
653 return self.__get_variant_soap_by_issue_or_episode(data = data, mode = u'issue')
654
656 return self.__get_variant_soap_by_issue_or_episode(data = data, mode = u'episode')
657
659
660
661 cats = list(u'soapu')
662 cats.append(None)
663
664 date_format = None
665 template = u'%s'
666
667 if data is not None:
668 data_parts = data.split('//')
669
670
671 if len(data_parts[0]) > 0:
672 cats = []
673 if u' ' in data_parts[0]:
674 cats.append(None)
675 cats.extend(list(data_parts[0].replace(u' ', u'')))
676
677
678 if len(data_parts) > 1:
679 if len(data_parts[1]) > 0:
680 date_format = data_parts[1]
681
682
683 if len(data_parts) > 2:
684 if len(data_parts[2]) > 0:
685 template = data_parts[2]
686
687 if mode == u'issue':
688 narr = gmNarrativeWidgets.select_narrative_by_issue(soap_cats = cats)
689 else:
690 narr = gmNarrativeWidgets.select_narrative_by_episode(soap_cats = cats)
691
692 if narr is None:
693 return u''
694
695 if len(narr) == 0:
696 return u''
697
698 try:
699 narr = [ template % n.fields_as_dict(date_format = date_format, escape_style = self.__esc_style) for n in narr ]
700 except KeyError:
701 return u'invalid key in template [%s], valid keys: %s]' % (template, str(narr[0].keys()))
702
703 return u'\n'.join(narr)
704
706 return self._get_variant_soap(data = data)
707
709 return self._get_variant_soap(data = u's')
710
712 return self._get_variant_soap(data = u'o')
713
715 return self._get_variant_soap(data = u'a')
716
718 return self._get_variant_soap(data = u'p')
719
721 return self._get_variant_soap(data = u'u')
722
724 return self._get_variant_soap(data = u' ')
725
727
728
729 cats = list(u'soapu')
730 cats.append(None)
731 template = u'%(narrative)s'
732
733 if data is not None:
734 data_parts = data.split('//')
735
736
737 cats = []
738
739 for cat in list(data_parts[0]):
740 if cat == u' ':
741 cat = None
742 cats.append(cat)
743
744 if cats == u'':
745 cats = list(u'soapu')
746 cats.append(None)
747
748
749 if len(data_parts) > 1:
750 template = data_parts[1]
751
752 narr = gmNarrativeWidgets.select_narrative_from_episodes(soap_cats = cats)
753
754 if narr is None:
755 return u''
756
757 if len(narr) == 0:
758 return u''
759
760
761
762
763 if u'%s' in template:
764 narr = [ self._escape(n['narrative']) for n in narr ]
765 else:
766 narr = [ n.fields_as_dict(escape_style = self.__esc_style) for n in narr ]
767
768 try:
769 narr = [ template % n for n in narr ]
770 except KeyError:
771 return u'invalid key in template [%s], valid keys: %s]' % (template, str(narr[0].keys()))
772 except TypeError:
773 return u'cannot mix "%%s" and "%%(key)s" in template [%s]' % template
774
775 return u'\n'.join(narr)
776
778 return self._get_variant_name(data = u'%(title)s')
779
781 return self._get_variant_name(data = u'%(firstnames)s')
782
784 return self._get_variant_name(data = u'%(lastnames)s')
785
787 if data is None:
788 return [_('template is missing')]
789
790 name = self.pat.get_active_name()
791
792 parts = {
793 'title': self._escape(gmTools.coalesce(name['title'], u'')),
794 'firstnames': self._escape(name['firstnames']),
795 'lastnames': self._escape(name['lastnames']),
796 'preferred': self._escape(gmTools.coalesce (
797 initial = name['preferred'],
798 instead = u' ',
799 template_initial = u' "%s" '
800 ))
801 }
802
803 return data % parts
804
807
808
810
811 values = data.split('//', 2)
812
813 if len(values) == 2:
814 male_value, female_value = values
815 other_value = u'<unkown gender>'
816 elif len(values) == 3:
817 male_value, female_value, other_value = values
818 else:
819 return _('invalid gender mapping layout: [%s]') % data
820
821 if self.pat['gender'] == u'm':
822 return self._escape(male_value)
823
824 if self.pat['gender'] == u'f':
825 return self._escape(female_value)
826
827 return self._escape(other_value)
828
829
830
832
833 data_parts = data.split(u'//')
834
835
836 adr_type = data_parts[0].strip()
837 orig_type = adr_type
838 if adr_type != u'':
839 adrs = self.pat.get_addresses(address_type = adr_type)
840 if len(adrs) == 0:
841 _log.warning('no address for type [%s]', adr_type)
842 adr_type = u''
843 if adr_type == u'':
844 _log.debug('asking user for address type')
845 adr = gmPersonContactWidgets.select_address(missing = orig_type, person = self.pat)
846 if adr is None:
847 if self.debug:
848 return _('no address type replacement selected')
849 return u''
850 adr_type = adr['address_type']
851 adr = self.pat.get_addresses(address_type = adr_type)[0]
852
853
854 template = _('%(street)s %(number)s, %(postcode)s %(urb)s, %(l10n_state)s, %(l10n_country)s')
855 if len(data_parts) > 1:
856 if data_parts[1].strip() != u'':
857 template = data_parts[1]
858
859 try:
860 return template % adr.fields_as_dict(escape_style = self.__esc_style)
861 except StandardError:
862 _log.exception('error formatting address')
863 _log.error('template: %s', template)
864
865 return None
866
868 requested_type = data.strip()
869 cache_key = 'adr-type-%s' % requested_type
870 try:
871 type2use = self.__cache[cache_key]
872 _log.debug('cache hit (%s): [%s] -> [%s]', cache_key, requested_type, type2use)
873 except KeyError:
874 type2use = requested_type
875 if type2use != u'':
876 adrs = self.pat.get_addresses(address_type = type2use)
877 if len(adrs) == 0:
878 _log.warning('no address of type [%s] for <%s> field extraction', requested_type, part)
879 type2use = u''
880 if type2use == u'':
881 _log.debug('asking user for replacement address type')
882 adr = gmPersonContactWidgets.select_address(missing = requested_type, person = self.pat)
883 if adr is None:
884 _log.debug('no replacement selected')
885 if self.debug:
886 return self._escape(_('no address type replacement selected'))
887 return u''
888 type2use = adr['address_type']
889 self.__cache[cache_key] = type2use
890 _log.debug('caching (%s): [%s] -> [%s]', cache_key, requested_type, type2use)
891
892 return self._escape(self.pat.get_addresses(address_type = type2use)[0][part])
893
895 return self.__get_variant_adr_part(data = data, part = 'street')
896
898 return self.__get_variant_adr_part(data = data, part = 'number')
899
901 return self.__get_variant_adr_part(data = data, part = 'subunit')
902
904 return self.__get_variant_adr_part(data = data, part = 'urb')
905
907 return self.__get_variant_adr_part(data = data, part = 'suburb')
908
909 - def _get_variant_adr_postcode(self, data=u'?'):
910 return self.__get_variant_adr_part(data = data, part = 'postcode')
911
913 return self.__get_variant_adr_part(data = data, part = 'l10n_state')
914
916 return self.__get_variant_adr_part(data = data, part = 'l10n_country')
917
919 comm_type = None
920 template = u'%(url)s'
921 if data is not None:
922 data_parts = data.split(u'//')
923 if len(data_parts) > 0:
924 comm_type = data_parts[0]
925 if len(data_parts) > 1:
926 template = data_parts[1]
927
928 comms = self.pat.get_comm_channels(comm_medium = comm_type)
929 if len(comms) == 0:
930 if self.debug:
931 return template + u': ' + self._escape(_('no URL for comm channel [%s]') % data)
932 return u''
933
934 return template % comms[0].fields_as_dict(escape_style = self.__esc_style)
935
937
938 template = u'%s'
939 target_mime = None
940 target_ext = None
941 if data is not None:
942 parts = data.split(u'//')
943 template = parts[0]
944 if len(parts) > 1:
945 target_mime = parts[1].strip()
946 if len(parts) > 2:
947 target_ext = parts[2].strip()
948 if target_ext is None:
949 if target_mime is not None:
950 target_ext = gmMimeLib.guess_ext_by_mimetype(mimetype = target_mime)
951
952 mugshot = self.pat.document_folder.latest_mugshot
953 if mugshot is None:
954 if self.debug:
955 return self._escape(_('no mugshot available'))
956 return u''
957
958 fname = mugshot.export_to_file (
959 target_mime = target_mime,
960 target_extension = target_ext,
961 ignore_conversion_problems = True
962 )
963 if fname is None:
964 if self.debug:
965 return self._escape(_('cannot export or convert latest mugshot'))
966 return u''
967
968 return template % fname
969
986
987
988
989
990
991
993 options = data.split(u'//')
994
995 if u'select' in options:
996 options.remove(u'select')
997 branch = 'select branch'
998 else:
999 branch = gmPraxis.cPraxisBranch(aPK_obj = gmPraxis.gmCurrentPraxisBranch()['pk_praxis_branch'])
1000
1001 template = u'%s'
1002 if len(options) > 0:
1003 template = options[0]
1004 if template.strip() == u'':
1005 template = u'%s'
1006
1007 return template % branch.fields_as_dict(escape_style = self.__esc_style)
1008
1009
1011
1012 options = data.split(u'//')
1013
1014
1015 template = _('%(street)s %(number)s, %(postcode)s %(urb)s, %(l10n_state)s, %(l10n_country)s')
1016 if len(options) > 0:
1017 if options[0].strip() != u'':
1018 template = options[0]
1019
1020 adr = gmPraxis.gmCurrentPraxisBranch().address
1021 if adr is None:
1022 if self.debug:
1023 return _('no address recorded')
1024 return u''
1025 try:
1026 return template % adr.fields_as_dict(escape_style = self.__esc_style)
1027 except StandardError:
1028 _log.exception('error formatting address')
1029 _log.error('template: %s', template)
1030
1031 return None
1032
1034 options = data.split(u'//')
1035 comm_type = options[0]
1036 template = u'%(url)s'
1037 if len(options) > 1:
1038 template = options[1]
1039
1040 comms = gmPraxis.gmCurrentPraxisBranch().get_comm_channels(comm_medium = comm_type)
1041 if len(comms) == 0:
1042 if self.debug:
1043 return template + u': ' + self._escape(_('no URL for comm channel [%s]') % data)
1044 return u''
1045
1046 return template % comms[0].fields_as_dict(escape_style = self.__esc_style)
1047
1048
1049
1064
1066 data_parts = data.split(u'//')
1067 if len(data_parts) < 2:
1068 return self._escape(u'current provider external ID: template is missing')
1069
1070 id_type = data_parts[0].strip()
1071 if id_type == u'':
1072 return self._escape(u'current provider external ID: type is missing')
1073
1074 issuer = data_parts[1].strip()
1075 if issuer == u'':
1076 return self._escape(u'current provider external ID: issuer is missing')
1077
1078 prov = gmStaff.gmCurrentProvider()
1079 ids = prov.identity.get_external_ids(id_type = id_type, issuer = issuer)
1080
1081 if len(ids) == 0:
1082 if self.debug:
1083 return self._escape(_('no external ID [%s] by [%s]') % (id_type, issuer))
1084 return u''
1085
1086 return self._escape(ids[0]['value'])
1087
1089 prov = self.pat.primary_provider
1090 if prov is None:
1091 return self._get_variant_current_provider()
1092
1093 title = gmTools.coalesce (
1094 prov['title'],
1095 gmPerson.map_gender2salutation(prov['gender'])
1096 )
1097
1098 tmp = u'%s %s. %s' % (
1099 title,
1100 prov['firstnames'][:1],
1101 prov['lastnames']
1102 )
1103 return self._escape(tmp)
1104
1106 data_parts = data.split(u'//')
1107 if len(data_parts) < 2:
1108 return self._escape(u'primary in-praxis provider external ID: template is missing')
1109
1110 id_type = data_parts[0].strip()
1111 if id_type == u'':
1112 return self._escape(u'primary in-praxis provider external ID: type is missing')
1113
1114 issuer = data_parts[1].strip()
1115 if issuer == u'':
1116 return self._escape(u'primary in-praxis provider external ID: issuer is missing')
1117
1118 prov = self.pat.primary_provider
1119 if prov is None:
1120 if self.debug:
1121 return self._escape(_('no primary in-praxis provider'))
1122 return u''
1123
1124 ids = prov.identity.get_external_ids(id_type = id_type, issuer = issuer)
1125
1126 if len(ids) == 0:
1127 if self.debug:
1128 return self._escape(_('no external ID [%s] by [%s]') % (id_type, issuer))
1129 return u''
1130
1131 return self._escape(ids[0]['value'])
1132
1134 data_parts = data.split(u'//')
1135 if len(data_parts) < 2:
1136 return self._escape(u'patient external ID: template is missing')
1137
1138 id_type = data_parts[0].strip()
1139 if id_type == u'':
1140 return self._escape(u'patient external ID: type is missing')
1141
1142 issuer = data_parts[1].strip()
1143 if issuer == u'':
1144 return self._escape(u'patient external ID: issuer is missing')
1145
1146 ids = self.pat.get_external_ids(id_type = id_type, issuer = issuer)
1147
1148 if len(ids) == 0:
1149 if self.debug:
1150 return self._escape(_('no external ID [%s] by [%s]') % (id_type, issuer))
1151 return u''
1152
1153 return self._escape(ids[0]['value'])
1154
1156 allg_state = self.pat.get_emr().allergy_state
1157
1158 if allg_state['last_confirmed'] is None:
1159 date_confirmed = u''
1160 else:
1161 date_confirmed = u' (%s)' % gmDateTime.pydt_strftime (
1162 allg_state['last_confirmed'],
1163 format = '%Y %B %d'
1164 )
1165
1166 tmp = u'%s%s' % (
1167 allg_state.state_string,
1168 date_confirmed
1169 )
1170 return self._escape(tmp)
1171
1173 if data is None:
1174 return self._escape(_('template is missing'))
1175
1176 template, separator = data.split('//', 2)
1177
1178 return separator.join([ template % a.fields_as_dict(date_format = '%Y %b %d', escape_style = self.__esc_style) for a in self.pat.emr.get_allergies() ])
1179
1186
1188 if data is None:
1189 return self._escape(_('current_meds_for_rx: template is missing'))
1190
1191 emr = self.pat.get_emr()
1192 from Gnumed.wxpython import gmMedicationWidgets
1193 current_meds = gmMedicationWidgets.manage_substance_intakes(emr = emr)
1194 if current_meds is None:
1195 return u''
1196
1197 intakes2show = {}
1198 for intake in current_meds:
1199 fields_dict = intake.fields_as_dict(date_format = '%Y %b %d', escape_style = self.__esc_style)
1200 if intake['pk_brand'] is None:
1201 fields_dict['brand'] = self._escape(_('generic %s') % fields_dict['substance'])
1202 fields_dict['contains'] = self._escape(u'%s %s%s' % (fields_dict['substance'], fields_dict['amount'], fields_dict['unit']))
1203 intakes2show[fields_dict['brand']] = fields_dict
1204 else:
1205 comps = [ c.split('::') for c in intake.containing_drug['components'] ]
1206 fields_dict['contains'] = self._escape(u'; '.join([ u'%s %s%s' % (c[0], c[1], c[2]) for c in comps ]))
1207 intakes2show[intake['brand']] = fields_dict
1208
1209 intakes2dispense = {}
1210 for brand, intake in intakes2show.items():
1211 msg = _('Dispense how much/many of "%(brand)s (%(contains)s)" ?') % intake
1212 amount2dispense = wx.GetTextFromUser(msg, _('Amount to dispense ?'))
1213 if amount2dispense == u'':
1214 continue
1215 intake['amount2dispense'] = amount2dispense
1216 intakes2dispense[brand] = intake
1217
1218 return u'\n'.join([ data % intake for intake in intakes2dispense.values() ])
1219
1247
1256
1265
1273
1275
1276 template = u''
1277 date_format = '%Y %b %d %H:%M'
1278 separator = u'\n'
1279
1280 options = data.split(u'//')
1281 try:
1282 template = options[0].strip()
1283 date_format = options[1]
1284 separator = options[2]
1285 except IndexError:
1286 pass
1287
1288 if date_format.strip() == u'':
1289 date_format = '%Y %b %d %H:%M'
1290 if separator.strip() == u'':
1291 separator = u'\n'
1292
1293 results = gmMeasurementWidgets.manage_measurements(single_selection = False, emr = self.pat.emr)
1294 if results is None:
1295 if self.debug:
1296 return self._escape(_('no results for this patient (available or selected)'))
1297 return u''
1298
1299 if template == u'':
1300 return (separator + separator).join([ self._escape(r.format(date_format = date_format)) for r in results ])
1301
1302 return separator.join([ template % r.fields_as_dict(date_format = date_format, escape_style = self.__esc_style) for r in results ])
1303
1311
1313 options = data.split('//')
1314 template = options[0]
1315 if len(options) > 1:
1316 date_format = options[1]
1317 else:
1318 date_format = u'%Y %b %d'
1319
1320 vaccs = self.pat.emr.get_vaccinations(order_by = u'date_given DESC, vaccine')
1321
1322 return u'\n'.join([ template % v.fields_as_dict(date_format = date_format, escape_style = self.__esc_style) for v in vaccs ])
1323
1325
1326 if data is None:
1327 if self.debug:
1328 _log.error('PHX: missing placeholder arguments')
1329 return self._escape(_('PHX: Invalid placeholder options.'))
1330 return u''
1331
1332 _log.debug('arguments: %s', data)
1333
1334 data_parts = data.split(u'//')
1335 template = u'%s'
1336 separator = u'\n'
1337 date_format = '%Y %b %d'
1338 esc_style = None
1339 try:
1340 template = data_parts[0]
1341 separator = data_parts[1]
1342 date_format = data_parts[2]
1343 esc_style = data_parts[3]
1344 except IndexError:
1345 pass
1346
1347 phxs = gmEMRStructWidgets.select_health_issues(emr = self.pat.emr)
1348 if phxs is None:
1349 if self.debug:
1350 return self._escape(_('no PHX for this patient (available or selected)'))
1351 return u''
1352
1353 return separator.join ([
1354 template % phx.fields_as_dict (
1355 date_format = date_format,
1356
1357 escape_style = self.__esc_style,
1358 bool_strings = (self._escape(_('yes')), self._escape(_('no')))
1359 ) for phx in phxs
1360 ])
1361
1363
1364 if data is None:
1365 return self._escape(_('template is missing'))
1366
1367 probs = self.pat.emr.get_problems()
1368
1369 return u'\n'.join([ data % p.fields_as_dict(date_format = '%Y %b %d', escape_style = self.__esc_style) for p in probs ])
1370
1373
1376
1377 - def _get_variant_text_snippet(self, data=None):
1378 data_parts = data.split(u'//')
1379 keyword = data_parts[0]
1380 template = u'%s'
1381 if len(data_parts) > 1:
1382 template = data_parts[1]
1383
1384 expansion = gmKeywordExpansionWidgets.expand_keyword(keyword = keyword, show_list = True)
1385
1386 if expansion is None:
1387 if self.debug:
1388 return self._escape(_('no textual expansion found for keyword <%s>') % keyword)
1389 return u''
1390
1391
1392 return template % expansion
1393
1449
1450 - def _get_variant_free_text(self, data=None):
1451
1452 if data is None:
1453 msg = _('generic text')
1454 else:
1455 msg = data
1456
1457 dlg = gmGuiHelpers.cMultilineTextEntryDlg (
1458 None,
1459 -1,
1460 title = _('Replacing <free_text> placeholder'),
1461 msg = _('Below you can enter free text.\n\n [%s]') % msg
1462 )
1463 dlg.enable_user_formatting = True
1464 decision = dlg.ShowModal()
1465
1466 if decision != wx.ID_SAVE:
1467 dlg.Destroy()
1468 if self.debug:
1469 return self._escape(_('Text input cancelled by user.'))
1470 return u''
1471
1472 text = dlg.value.strip()
1473 if dlg.is_user_formatted:
1474 dlg.Destroy()
1475 return text
1476
1477 dlg.Destroy()
1478
1479 return self._escape(text)
1480
1494
1508
1509
1510
1512 if self.__esc_func is None:
1513 return text
1514 return self.__esc_func(text)
1515
1517 """Functions a macro can legally use.
1518
1519 An instance of this class is passed to the GNUmed scripting
1520 listener. Hence, all actions a macro can legally take must
1521 be defined in this class. Thus we achieve some screening for
1522 security and also thread safety handling.
1523 """
1524
1525 - def __init__(self, personality = None):
1526 if personality is None:
1527 raise gmExceptions.ConstructorError, 'must specify personality'
1528 self.__personality = personality
1529 self.__attached = 0
1530 self._get_source_personality = None
1531 self.__user_done = False
1532 self.__user_answer = 'no answer yet'
1533 self.__pat = gmPerson.gmCurrentPatient()
1534
1535 self.__auth_cookie = str(random.random())
1536 self.__pat_lock_cookie = str(random.random())
1537 self.__lock_after_load_cookie = str(random.random())
1538
1539 _log.info('slave mode personality is [%s]', personality)
1540
1541
1542
1543 - def attach(self, personality = None):
1544 if self.__attached:
1545 _log.error('attach with [%s] rejected, already serving a client', personality)
1546 return (0, _('attach rejected, already serving a client'))
1547 if personality != self.__personality:
1548 _log.error('rejecting attach to personality [%s], only servicing [%s]' % (personality, self.__personality))
1549 return (0, _('attach to personality [%s] rejected') % personality)
1550 self.__attached = 1
1551 self.__auth_cookie = str(random.random())
1552 return (1, self.__auth_cookie)
1553
1554 - def detach(self, auth_cookie=None):
1555 if not self.__attached:
1556 return 1
1557 if auth_cookie != self.__auth_cookie:
1558 _log.error('rejecting detach() with cookie [%s]' % auth_cookie)
1559 return 0
1560 self.__attached = 0
1561 return 1
1562
1564 if not self.__attached:
1565 return 1
1566 self.__user_done = False
1567
1568 wx.CallAfter(self._force_detach)
1569 return 1
1570
1572 ver = _cfg.get(option = u'client_version')
1573 return "GNUmed %s, %s $Revision: 1.51 $" % (ver, self.__class__.__name__)
1574
1576 """Shuts down this client instance."""
1577 if not self.__attached:
1578 return 0
1579 if auth_cookie != self.__auth_cookie:
1580 _log.error('non-authenticated shutdown_gnumed()')
1581 return 0
1582 wx.CallAfter(self._shutdown_gnumed, forced)
1583 return 1
1584
1586 """Raise ourselves to the top of the desktop."""
1587 if not self.__attached:
1588 return 0
1589 if auth_cookie != self.__auth_cookie:
1590 _log.error('non-authenticated raise_gnumed()')
1591 return 0
1592 return "cMacroPrimitives.raise_gnumed() not implemented"
1593
1595 if not self.__attached:
1596 return 0
1597 if auth_cookie != self.__auth_cookie:
1598 _log.error('non-authenticated get_loaded_plugins()')
1599 return 0
1600 gb = gmGuiBroker.GuiBroker()
1601 return gb['horstspace.notebook.gui'].keys()
1602
1604 """Raise a notebook plugin within GNUmed."""
1605 if not self.__attached:
1606 return 0
1607 if auth_cookie != self.__auth_cookie:
1608 _log.error('non-authenticated raise_notebook_plugin()')
1609 return 0
1610
1611 wx.CallAfter(gmPlugin.raise_notebook_plugin, a_plugin)
1612 return 1
1613
1615 """Load external patient, perhaps create it.
1616
1617 Callers must use get_user_answer() to get status information.
1618 It is unsafe to proceed without knowing the completion state as
1619 the controlled client may be waiting for user input from a
1620 patient selection list.
1621 """
1622 if not self.__attached:
1623 return (0, _('request rejected, you are not attach()ed'))
1624 if auth_cookie != self.__auth_cookie:
1625 _log.error('non-authenticated load_patient_from_external_source()')
1626 return (0, _('rejected load_patient_from_external_source(), not authenticated'))
1627 if self.__pat.locked:
1628 _log.error('patient is locked, cannot load from external source')
1629 return (0, _('current patient is locked'))
1630 self.__user_done = False
1631 wx.CallAfter(self._load_patient_from_external_source)
1632 self.__lock_after_load_cookie = str(random.random())
1633 return (1, self.__lock_after_load_cookie)
1634
1636 if not self.__attached:
1637 return (0, _('request rejected, you are not attach()ed'))
1638 if auth_cookie != self.__auth_cookie:
1639 _log.error('non-authenticated lock_load_patient()')
1640 return (0, _('rejected lock_load_patient(), not authenticated'))
1641
1642 if lock_after_load_cookie != self.__lock_after_load_cookie:
1643 _log.warning('patient lock-after-load request rejected due to wrong cookie [%s]' % lock_after_load_cookie)
1644 return (0, 'patient lock-after-load request rejected, wrong cookie provided')
1645 self.__pat.locked = True
1646 self.__pat_lock_cookie = str(random.random())
1647 return (1, self.__pat_lock_cookie)
1648
1650 if not self.__attached:
1651 return (0, _('request rejected, you are not attach()ed'))
1652 if auth_cookie != self.__auth_cookie:
1653 _log.error('non-authenticated lock_into_patient()')
1654 return (0, _('rejected lock_into_patient(), not authenticated'))
1655 if self.__pat.locked:
1656 _log.error('patient is already locked')
1657 return (0, _('already locked into a patient'))
1658 searcher = gmPersonSearch.cPatientSearcher_SQL()
1659 if type(search_params) == types.DictType:
1660 idents = searcher.get_identities(search_dict=search_params)
1661 raise StandardError("must use dto, not search_dict")
1662 else:
1663 idents = searcher.get_identities(search_term=search_params)
1664 if idents is None:
1665 return (0, _('error searching for patient with [%s]/%s') % (search_term, search_dict))
1666 if len(idents) == 0:
1667 return (0, _('no patient found for [%s]/%s') % (search_term, search_dict))
1668
1669 if len(idents) > 1:
1670 return (0, _('several matching patients found for [%s]/%s') % (search_term, search_dict))
1671 if not gmPatSearchWidgets.set_active_patient(patient = idents[0]):
1672 return (0, _('cannot activate patient [%s] (%s/%s)') % (str(idents[0]), search_term, search_dict))
1673 self.__pat.locked = True
1674 self.__pat_lock_cookie = str(random.random())
1675 return (1, self.__pat_lock_cookie)
1676
1678 if not self.__attached:
1679 return (0, _('request rejected, you are not attach()ed'))
1680 if auth_cookie != self.__auth_cookie:
1681 _log.error('non-authenticated unlock_patient()')
1682 return (0, _('rejected unlock_patient, not authenticated'))
1683
1684 if not self.__pat.locked:
1685 return (1, '')
1686
1687 if unlock_cookie != self.__pat_lock_cookie:
1688 _log.warning('patient unlock request rejected due to wrong cookie [%s]' % unlock_cookie)
1689 return (0, 'patient unlock request rejected, wrong cookie provided')
1690 self.__pat.locked = False
1691 return (1, '')
1692
1694 if not self.__attached:
1695 return 0
1696 if auth_cookie != self.__auth_cookie:
1697 _log.error('non-authenticated select_identity()')
1698 return 0
1699 return "cMacroPrimitives.assume_staff_identity() not implemented"
1700
1702 if not self.__user_done:
1703 return (0, 'still waiting')
1704 self.__user_done = False
1705 return (1, self.__user_answer)
1706
1707
1708
1710 msg = _(
1711 'Someone tries to forcibly break the existing\n'
1712 'controlling connection. This may or may not\n'
1713 'have legitimate reasons.\n\n'
1714 'Do you want to allow breaking the connection ?'
1715 )
1716 can_break_conn = gmGuiHelpers.gm_show_question (
1717 aMessage = msg,
1718 aTitle = _('forced detach attempt')
1719 )
1720 if can_break_conn:
1721 self.__user_answer = 1
1722 else:
1723 self.__user_answer = 0
1724 self.__user_done = True
1725 if can_break_conn:
1726 self.__pat.locked = False
1727 self.__attached = 0
1728 return 1
1729
1731 top_win = wx.GetApp().GetTopWindow()
1732 if forced:
1733 top_win.Destroy()
1734 else:
1735 top_win.Close()
1736
1745
1746
1747
1748 if __name__ == '__main__':
1749
1750 if len(sys.argv) < 2:
1751 sys.exit()
1752
1753 if sys.argv[1] != 'test':
1754 sys.exit()
1755
1756 gmI18N.activate_locale()
1757 gmI18N.install_domain()
1758
1759
1761 handler = gmPlaceholderHandler()
1762 handler.debug = True
1763
1764 for placeholder in ['a', 'b']:
1765 print handler[placeholder]
1766
1767 pat = gmPersonSearch.ask_for_patient()
1768 if pat is None:
1769 return
1770
1771 gmPatSearchWidgets.set_active_patient(patient = pat)
1772
1773 print 'DOB (YYYY-MM-DD):', handler['date_of_birth::%Y-%m-%d']
1774
1775 app = wx.PyWidgetTester(size = (200, 50))
1776
1777 ph = 'progress_notes::ap'
1778 print '%s: %s' % (ph, handler[ph])
1779
1781
1782 tests = [
1783
1784 '$<lastname>$',
1785 '$<lastname::::3>$',
1786 '$<name::%(title)s %(firstnames)s%(preferred)s%(lastnames)s>$',
1787
1788
1789 'lastname',
1790 '$<lastname',
1791 '$<lastname::',
1792 '$<lastname::>$',
1793 '$<lastname::abc>$',
1794 '$<lastname::abc::>$',
1795 '$<lastname::abc::3>$',
1796 '$<lastname::abc::xyz>$',
1797 '$<lastname::::>$',
1798 '$<lastname::::xyz>$',
1799
1800 '$<date_of_birth::%Y-%m-%d>$',
1801 '$<date_of_birth::%Y-%m-%d::3>$',
1802 '$<date_of_birth::%Y-%m-%d::>$',
1803
1804
1805 '$<adr_location::home::35>$',
1806 '$<gender_mapper::male//female//other::5>$',
1807 '$<current_meds::==> %(brand)s %(preparation)s (%(substance)s) <==\n::50>$',
1808 '$<allergy_list::%(descriptor)s, >$',
1809 '$<current_meds_table::latex//by-brand>$'
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824 ]
1825
1826
1827
1828
1829
1830 pat = gmPersonSearch.ask_for_patient()
1831 if pat is None:
1832 return
1833
1834 gmPatSearchWidgets.set_active_patient(patient = pat)
1835
1836 handler = gmPlaceholderHandler()
1837 handler.debug = True
1838
1839 for placeholder in tests:
1840 print placeholder, "=>", handler[placeholder]
1841 print "--------------"
1842 raw_input()
1843
1844
1845
1846
1847
1848
1849
1850
1851
1853 from Gnumed.pycommon import gmScriptingListener
1854 import xmlrpclib
1855 listener = gmScriptingListener.cScriptingListener(macro_executor = cMacroPrimitives(personality='unit test'), port=9999)
1856
1857 s = xmlrpclib.ServerProxy('http://localhost:9999')
1858 print "should fail:", s.attach()
1859 print "should fail:", s.attach('wrong cookie')
1860 print "should work:", s.version()
1861 print "should fail:", s.raise_gnumed()
1862 print "should fail:", s.raise_notebook_plugin('test plugin')
1863 print "should fail:", s.lock_into_patient('kirk, james')
1864 print "should fail:", s.unlock_patient()
1865 status, conn_auth = s.attach('unit test')
1866 print "should work:", status, conn_auth
1867 print "should work:", s.version()
1868 print "should work:", s.raise_gnumed(conn_auth)
1869 status, pat_auth = s.lock_into_patient(conn_auth, 'kirk, james')
1870 print "should work:", status, pat_auth
1871 print "should fail:", s.unlock_patient(conn_auth, 'bogus patient unlock cookie')
1872 print "should work", s.unlock_patient(conn_auth, pat_auth)
1873 data = {'firstname': 'jame', 'lastnames': 'Kirk', 'gender': 'm'}
1874 status, pat_auth = s.lock_into_patient(conn_auth, data)
1875 print "should work:", status, pat_auth
1876 print "should work", s.unlock_patient(conn_auth, pat_auth)
1877 print s.detach('bogus detach cookie')
1878 print s.detach(conn_auth)
1879 del s
1880
1881 listener.shutdown()
1882
1884
1885 import re as regex
1886
1887 tests = [
1888 ' $<lastname>$ ',
1889 ' $<lastname::::3>$ ',
1890
1891
1892 '$<date_of_birth::%Y-%m-%d>$',
1893 '$<date_of_birth::%Y-%m-%d::3>$',
1894 '$<date_of_birth::%Y-%m-%d::>$',
1895
1896 '$<adr_location::home::35>$',
1897 '$<gender_mapper::male//female//other::5>$',
1898 '$<current_meds::==> %(brand)s %(preparation)s (%(substance)s) <==\\n::50>$',
1899 '$<allergy_list::%(descriptor)s, >$',
1900
1901 '\\noindent Patient: $<lastname>$, $<firstname>$',
1902 '$<allergies::%(descriptor)s & %(l10n_type)s & {\\footnotesize %(reaction)s} \tabularnewline \hline >$',
1903 '$<current_meds:: \item[%(substance)s] {\\footnotesize (%(brand)s)} %(preparation)s %(amount)s%(unit)s: %(schedule)s >$'
1904 ]
1905
1906 tests = [
1907
1908 'junk $<lastname::::3>$ junk',
1909 'junk $<lastname::abc::3>$ junk',
1910 'junk $<lastname::abc>$ junk',
1911 'junk $<lastname>$ junk',
1912
1913 'junk $<lastname>$ junk $<firstname>$ junk',
1914 'junk $<lastname::abc>$ junk $<fiststname::abc>$ junk',
1915 'junk $<lastname::abc::3>$ junk $<firstname::abc::3>$ junk',
1916 'junk $<lastname::::3>$ junk $<firstname::::3>$ junk'
1917
1918 ]
1919
1920 tests = [
1921
1922
1923
1924
1925
1926 u'$<<<current_meds::%(brand)s (%(substance)s): Dispense $<free_text::Dispense how many of %(brand)s %(preparation)s (%(substance)s) ?::20>$ (%(preparation)s) \\n::250>>>$',
1927 ]
1928
1929 print "testing placeholder regex:", first_order_placeholder_regex
1930 print ""
1931
1932 for t in tests:
1933 print 'line: "%s"' % t
1934 phs = regex.findall(first_order_placeholder_regex, t, regex.IGNORECASE)
1935 print " %s placeholders:" % len(phs)
1936 for p in phs:
1937 print ' => "%s"' % p
1938 print " "
1939
2006
2007
2015
2018
2019
2020
2021
2022
2023
2024
2025 test_placeholder()
2026
2027
2028
2029