1 """GNUmed macro primitives.
2
3 This module implements functions a macro can legally use.
4 """
5
6 __version__ = "$Revision: 1.51 $"
7 __author__ = "K.Hilbert <karsten.hilbert@gmx.net>"
8
9 import sys, time, random, types, logging
10
11
12 import wx
13
14
15 if __name__ == '__main__':
16 sys.path.insert(0, '../../')
17 from Gnumed.pycommon import gmI18N, gmGuiBroker, gmExceptions, gmBorg, gmTools
18 from Gnumed.pycommon import gmCfg2, gmDateTime
19 from Gnumed.business import gmPerson, gmDemographicRecord, gmMedication, gmPathLab, gmPersonSearch
20 from Gnumed.business import gmVaccination, gmPersonSearch
21 from Gnumed.wxpython import gmGuiHelpers, gmPlugin, gmPatSearchWidgets, gmNarrativeWidgets
22
23
24 _log = logging.getLogger('gm.scripting')
25 _cfg = gmCfg2.gmCfgData()
26
27
28 known_placeholders = [
29 'lastname',
30 'firstname',
31 'title',
32 'date_of_birth',
33 'progress_notes',
34 'soap',
35 'soap_s',
36 'soap_o',
37 'soap_a',
38 'soap_p',
39 u'client_version',
40 u'current_provider',
41 u'allergy_state'
42 ]
43
44
45
46 known_variant_placeholders = [
47 u'soap',
48 u'progress_notes',
49
50
51 u'emr_journal',
52
53
54
55
56 u'date_of_birth',
57 u'adr_street',
58 u'adr_number',
59 u'adr_location',
60 u'adr_postcode',
61 u'gender_mapper',
62 u'current_meds',
63 u'current_meds_table',
64 u'current_meds_notes',
65 u'lab_table',
66 u'latest_vaccs_table',
67 u'today',
68 u'tex_escape',
69 u'allergies',
70 u'allergy_list',
71 u'problems',
72 u'name'
73 ]
74
75 default_placeholder_regex = r'\$<.+?>\$'
76
77
78
79
80
81
82
83
84 default_placeholder_start = u'$<'
85 default_placeholder_end = u'>$'
86
88 """Replaces placeholders in forms, fields, etc.
89
90 - patient related placeholders operate on the currently active patient
91 - is passed to the forms handling code, for example
92
93 Note that this cannot be called from a non-gui thread unless
94 wrapped in wx.CallAfter.
95
96 There are currently three types of placeholders:
97
98 simple static placeholders
99 - those are listed in known_placeholders
100 - they are used as-is
101
102 extended static placeholders
103 - those are like the static ones but have "::::<NUMBER>" appended
104 where <NUMBER> is the maximum length
105
106 variant placeholders
107 - those are listed in known_variant_placeholders
108 - they are parsed into placeholder, data, and maximum length
109 - the length is optional
110 - data is passed to the handler
111 """
113
114 self.pat = gmPerson.gmCurrentPatient()
115 self.debug = False
116
117 self.invalid_placeholder_template = _('invalid placeholder [%s]')
118
119
120
122 """Map self['placeholder'] to self.placeholder.
123
124 This is useful for replacing placeholders parsed out
125 of documents as strings.
126
127 Unknown/invalid placeholders still deliver a result but
128 it will be glaringly obvious if debugging is enabled.
129 """
130 _log.debug('replacing [%s]', placeholder)
131
132 original_placeholder = placeholder
133
134 if placeholder.startswith(default_placeholder_start):
135 placeholder = placeholder[len(default_placeholder_start):]
136 if placeholder.endswith(default_placeholder_end):
137 placeholder = placeholder[:-len(default_placeholder_end)]
138 else:
139 _log.debug('placeholder must either start with [%s] and end with [%s] or neither of both', default_placeholder_start, default_placeholder_end)
140 if self.debug:
141 return self.invalid_placeholder_template % original_placeholder
142 return None
143
144
145 if placeholder in known_placeholders:
146 return getattr(self, placeholder)
147
148
149 parts = placeholder.split('::::', 1)
150 if len(parts) == 2:
151 name, lng = parts
152 try:
153 return getattr(self, name)[:int(lng)]
154 except:
155 _log.exception('placeholder handling error: %s', original_placeholder)
156 if self.debug:
157 return self.invalid_placeholder_template % original_placeholder
158 return None
159
160
161 parts = placeholder.split('::')
162 if len(parts) == 2:
163 name, data = parts
164 lng = None
165 if len(parts) == 3:
166 name, data, lng = parts
167 try:
168 lng = int(lng)
169 except:
170 _log.exception('placeholder length definition error: %s, discarding length', original_placeholder)
171 lng = None
172 if len(parts) > 3:
173 _log.warning('invalid placeholder layout: %s', original_placeholder)
174 if self.debug:
175 return self.invalid_placeholder_template % original_placeholder
176 return None
177
178 handler = getattr(self, '_get_variant_%s' % name, None)
179 if handler is None:
180 _log.warning('no handler <_get_variant_%s> for placeholder %s', name, original_placeholder)
181 if self.debug:
182 return self.invalid_placeholder_template % original_placeholder
183 return None
184
185 try:
186 if lng is None:
187 return handler(data = data)
188 return handler(data = data)[:lng]
189 except:
190 _log.exception('placeholder handling error: %s', original_placeholder)
191 if self.debug:
192 return self.invalid_placeholder_template % original_placeholder
193 return None
194
195 _log.error('something went wrong, should never get here')
196 return None
197
198
199
200
201
203 """This does nothing, used as a NOOP properties setter."""
204 pass
205
208
211
214
216 return self._get_variant_date_of_birth(data='%x')
217
219 return self._get_variant_soap()
220
222 return self._get_variant_soap(data = u's')
223
225 return self._get_variant_soap(data = u'o')
226
228 return self._get_variant_soap(data = u'a')
229
231 return self._get_variant_soap(data = u'p')
232
234 return self._get_variant_soap(soap_cats = None)
235
237 return gmTools.coalesce (
238 _cfg.get(option = u'client_version'),
239 u'%s' % self.__class__.__name__
240 )
241
257
259 allg_state = self.pat.get_emr().allergy_state
260
261 if allg_state['last_confirmed'] is None:
262 date_confirmed = u''
263 else:
264 date_confirmed = u' (%s)' % allg_state['last_confirmed'].strftime('%Y %B %d').decode(gmI18N.get_encoding())
265
266 tmp = u'%s%s' % (
267 allg_state.state_string,
268 date_confirmed
269 )
270 return tmp
271
272
273
274 placeholder_regex = property(lambda x: default_placeholder_regex, _setter_noop)
275
276
277 lastname = property(_get_lastname, _setter_noop)
278 firstname = property(_get_firstname, _setter_noop)
279 title = property(_get_title, _setter_noop)
280 date_of_birth = property(_get_dob, _setter_noop)
281
282 progress_notes = property(_get_progress_notes, _setter_noop)
283 soap = property(_get_progress_notes, _setter_noop)
284 soap_s = property(_get_soap_s, _setter_noop)
285 soap_o = property(_get_soap_o, _setter_noop)
286 soap_a = property(_get_soap_a, _setter_noop)
287 soap_p = property(_get_soap_p, _setter_noop)
288 soap_admin = property(_get_soap_admin, _setter_noop)
289
290 allergy_state = property(_get_allergy_state, _setter_noop)
291
292 client_version = property(_get_client_version, _setter_noop)
293
294 current_provider = property(_get_current_provider, _setter_noop)
295
296
297
299
300 cats = list(u'soap').append(None)
301 template = u'%s'
302 interactive = True
303 line_length = 9999
304 target_format = None
305
306 if data is not None:
307 data_parts = data.split('//')
308
309
310 cats = []
311
312 for c in list(data_parts[0]):
313 if c == u' ':
314 c = None
315 cats.append(c)
316
317 if cats == u'':
318 cats = cats = list(u'soap').append(None)
319
320
321 if len(data_parts) > 0:
322 template = data_parts[1]
323
324
325 if len(data_parts) > 1:
326 try:
327 line_length = int(data_parts[2])
328 except:
329 line_length = 9999
330
331
332 if len(data_parts) > 2:
333 target_format = data_parts[3]
334
335
336 narr = self.pat.get_emr().get_as_journal(soap_cats = cats)
337
338 if len(narr) == 0:
339 return u''
340
341 if target_format == u'tex':
342 keys = narr[0].keys()
343 lines = []
344 line_dict = {}
345 for n in narr:
346 for key in keys:
347 if isinstance(n[key], basestring):
348 line_dict[key] = gmTools.tex_escape_string(text = n[key])
349 continue
350 line_dict[key] = n[key]
351 try:
352 lines.append((template % line_dict)[:line_length])
353 except KeyError:
354 return u'invalid key in template [%s], valid keys: %s]' % (template, str(keys))
355 else:
356 try:
357 lines = [ (template % n)[:line_length] for n in narr ]
358 except KeyError:
359 return u'invalid key in template [%s], valid keys: %s]' % (template, str(narr[0].keys()))
360
361 return u'\n'.join(lines)
362
364 return self._get_variant_soap(data=data)
365
367
368
369 cats = list(u'soap').append(None)
370 template = u'%s'
371
372 if data is not None:
373 data_parts = data.split('//')
374
375
376 cats = []
377
378 for c in list(data_parts[0]):
379 if c == u' ':
380 c = None
381 cats.append(c)
382
383 if cats == u'':
384 cats = cats = list(u'soap').append(None)
385
386
387 if len(data_parts) > 0:
388 template = data_parts[1]
389
390 narr = gmNarrativeWidgets.select_narrative_from_episodes(soap_cats = cats)
391
392 if len(narr) == 0:
393 return u''
394
395 try:
396 narr = [ template % n['narrative'] for n in narr ]
397 except KeyError:
398 return u'invalid key in template [%s], valid keys: %s]' % (template, str(narr[0].keys()))
399
400 return u'\n'.join(narr)
401
420
423
424
426 values = data.split('//', 2)
427
428 if len(values) == 2:
429 male_value, female_value = values
430 other_value = u'<unkown gender>'
431 elif len(values) == 3:
432 male_value, female_value, other_value = values
433 else:
434 return _('invalid gender mapping layout: [%s]') % data
435
436 if self.pat['gender'] == u'm':
437 return male_value
438
439 if self.pat['gender'] == u'f':
440 return female_value
441
442 return other_value
443
445
446
447 adrs = self.pat.get_addresses(address_type=data)
448 if len(adrs) == 0:
449 return _('no street for address type [%s]') % data
450 return adrs[0]['street']
451
453 adrs = self.pat.get_addresses(address_type=data)
454 if len(adrs) == 0:
455 return _('no number for address type [%s]') % data
456 return adrs[0]['number']
457
459 adrs = self.pat.get_addresses(address_type=data)
460 if len(adrs) == 0:
461 return _('no location for address type [%s]') % data
462 return adrs[0]['urb']
463
464 - def _get_variant_adr_postcode(self, data=u'?'):
465 adrs = self.pat.get_addresses(address_type=data)
466 if len(adrs) == 0:
467 return _('no postcode for address type [%s]') % data
468 return adrs[0]['postcode']
469
471 if data is None:
472 return [_('template is missing')]
473
474 template, separator = data.split('//', 2)
475
476 emr = self.pat.get_emr()
477 return separator.join([ template % a for a in emr.get_allergies() ])
478
480
481 if data is None:
482 return [_('template is missing')]
483
484 emr = self.pat.get_emr()
485 return u'\n'.join([ data % a for a in emr.get_allergies() ])
486
488
489 if data is None:
490 return [_('template is missing')]
491
492 emr = self.pat.get_emr()
493 current_meds = emr.get_current_substance_intake (
494 include_inactive = False,
495 include_unapproved = False,
496 order_by = u'brand, substance'
497 )
498
499
500
501 return u'\n'.join([ data % m for m in current_meds ])
502
504
505 options = data.split('//')
506
507 if u'latex' in options:
508 return gmMedication.format_substance_intake (
509 emr = self.pat.get_emr(),
510 output_format = u'latex',
511 table_type = u'by-brand'
512 )
513
514 _log.error('no known current medications table formatting style in [%]', data)
515 return _('unknown current medication table formatting style')
516
518
519 options = data.split('//')
520
521 if u'latex' in options:
522 return gmMedication.format_substance_intake_notes (
523 emr = self.pat.get_emr(),
524 output_format = u'latex',
525 table_type = u'by-brand'
526 )
527
528 _log.error('no known current medications notes formatting style in [%]', data)
529 return _('unknown current medication notes formatting style')
530
545
547
548 options = data.split('//')
549
550 emr = self.pat.get_emr()
551
552 if u'latex' in options:
553 return gmVaccination.format_latest_vaccinations(output_format = u'latex', emr = emr)
554
555 _log.error('no known vaccinations table formatting style in [%s]', data)
556 return _('unknown vaccinations table formatting style [%s]') % data
557
559
560 if data is None:
561 return [_('template is missing')]
562
563 probs = self.pat.get_emr().get_problems()
564
565 return u'\n'.join([ data % p for p in probs ])
566
569
572
573
574
575
576
578 """Functions a macro can legally use.
579
580 An instance of this class is passed to the GNUmed scripting
581 listener. Hence, all actions a macro can legally take must
582 be defined in this class. Thus we achieve some screening for
583 security and also thread safety handling.
584 """
585
586 - def __init__(self, personality = None):
587 if personality is None:
588 raise gmExceptions.ConstructorError, 'must specify personality'
589 self.__personality = personality
590 self.__attached = 0
591 self._get_source_personality = None
592 self.__user_done = False
593 self.__user_answer = 'no answer yet'
594 self.__pat = gmPerson.gmCurrentPatient()
595
596 self.__auth_cookie = str(random.random())
597 self.__pat_lock_cookie = str(random.random())
598 self.__lock_after_load_cookie = str(random.random())
599
600 _log.info('slave mode personality is [%s]', personality)
601
602
603
604 - def attach(self, personality = None):
605 if self.__attached:
606 _log.error('attach with [%s] rejected, already serving a client', personality)
607 return (0, _('attach rejected, already serving a client'))
608 if personality != self.__personality:
609 _log.error('rejecting attach to personality [%s], only servicing [%s]' % (personality, self.__personality))
610 return (0, _('attach to personality [%s] rejected') % personality)
611 self.__attached = 1
612 self.__auth_cookie = str(random.random())
613 return (1, self.__auth_cookie)
614
615 - def detach(self, auth_cookie=None):
616 if not self.__attached:
617 return 1
618 if auth_cookie != self.__auth_cookie:
619 _log.error('rejecting detach() with cookie [%s]' % auth_cookie)
620 return 0
621 self.__attached = 0
622 return 1
623
625 if not self.__attached:
626 return 1
627 self.__user_done = False
628
629 wx.CallAfter(self._force_detach)
630 return 1
631
633 ver = _cfg.get(option = u'client_version')
634 return "GNUmed %s, %s $Revision: 1.51 $" % (ver, self.__class__.__name__)
635
637 """Shuts down this client instance."""
638 if not self.__attached:
639 return 0
640 if auth_cookie != self.__auth_cookie:
641 _log.error('non-authenticated shutdown_gnumed()')
642 return 0
643 wx.CallAfter(self._shutdown_gnumed, forced)
644 return 1
645
647 """Raise ourselves to the top of the desktop."""
648 if not self.__attached:
649 return 0
650 if auth_cookie != self.__auth_cookie:
651 _log.error('non-authenticated raise_gnumed()')
652 return 0
653 return "cMacroPrimitives.raise_gnumed() not implemented"
654
656 if not self.__attached:
657 return 0
658 if auth_cookie != self.__auth_cookie:
659 _log.error('non-authenticated get_loaded_plugins()')
660 return 0
661 gb = gmGuiBroker.GuiBroker()
662 return gb['horstspace.notebook.gui'].keys()
663
665 """Raise a notebook plugin within GNUmed."""
666 if not self.__attached:
667 return 0
668 if auth_cookie != self.__auth_cookie:
669 _log.error('non-authenticated raise_notebook_plugin()')
670 return 0
671
672 wx.CallAfter(gmPlugin.raise_notebook_plugin, a_plugin)
673 return 1
674
676 """Load external patient, perhaps create it.
677
678 Callers must use get_user_answer() to get status information.
679 It is unsafe to proceed without knowing the completion state as
680 the controlled client may be waiting for user input from a
681 patient selection list.
682 """
683 if not self.__attached:
684 return (0, _('request rejected, you are not attach()ed'))
685 if auth_cookie != self.__auth_cookie:
686 _log.error('non-authenticated load_patient_from_external_source()')
687 return (0, _('rejected load_patient_from_external_source(), not authenticated'))
688 if self.__pat.locked:
689 _log.error('patient is locked, cannot load from external source')
690 return (0, _('current patient is locked'))
691 self.__user_done = False
692 wx.CallAfter(self._load_patient_from_external_source)
693 self.__lock_after_load_cookie = str(random.random())
694 return (1, self.__lock_after_load_cookie)
695
697 if not self.__attached:
698 return (0, _('request rejected, you are not attach()ed'))
699 if auth_cookie != self.__auth_cookie:
700 _log.error('non-authenticated lock_load_patient()')
701 return (0, _('rejected lock_load_patient(), not authenticated'))
702
703 if lock_after_load_cookie != self.__lock_after_load_cookie:
704 _log.warning('patient lock-after-load request rejected due to wrong cookie [%s]' % lock_after_load_cookie)
705 return (0, 'patient lock-after-load request rejected, wrong cookie provided')
706 self.__pat.locked = True
707 self.__pat_lock_cookie = str(random.random())
708 return (1, self.__pat_lock_cookie)
709
711 if not self.__attached:
712 return (0, _('request rejected, you are not attach()ed'))
713 if auth_cookie != self.__auth_cookie:
714 _log.error('non-authenticated lock_into_patient()')
715 return (0, _('rejected lock_into_patient(), not authenticated'))
716 if self.__pat.locked:
717 _log.error('patient is already locked')
718 return (0, _('already locked into a patient'))
719 searcher = gmPersonSearch.cPatientSearcher_SQL()
720 if type(search_params) == types.DictType:
721 idents = searcher.get_identities(search_dict=search_params)
722 print "must use dto, not search_dict"
723 print xxxxxxxxxxxxxxxxx
724 else:
725 idents = searcher.get_identities(search_term=search_params)
726 if idents is None:
727 return (0, _('error searching for patient with [%s]/%s') % (search_term, search_dict))
728 if len(idents) == 0:
729 return (0, _('no patient found for [%s]/%s') % (search_term, search_dict))
730
731 if len(idents) > 1:
732 return (0, _('several matching patients found for [%s]/%s') % (search_term, search_dict))
733 if not gmPatSearchWidgets.set_active_patient(patient = idents[0]):
734 return (0, _('cannot activate patient [%s] (%s/%s)') % (str(idents[0]), search_term, search_dict))
735 self.__pat.locked = True
736 self.__pat_lock_cookie = str(random.random())
737 return (1, self.__pat_lock_cookie)
738
740 if not self.__attached:
741 return (0, _('request rejected, you are not attach()ed'))
742 if auth_cookie != self.__auth_cookie:
743 _log.error('non-authenticated unlock_patient()')
744 return (0, _('rejected unlock_patient, not authenticated'))
745
746 if not self.__pat.locked:
747 return (1, '')
748
749 if unlock_cookie != self.__pat_lock_cookie:
750 _log.warning('patient unlock request rejected due to wrong cookie [%s]' % unlock_cookie)
751 return (0, 'patient unlock request rejected, wrong cookie provided')
752 self.__pat.locked = False
753 return (1, '')
754
756 if not self.__attached:
757 return 0
758 if auth_cookie != self.__auth_cookie:
759 _log.error('non-authenticated select_identity()')
760 return 0
761 return "cMacroPrimitives.assume_staff_identity() not implemented"
762
764 if not self.__user_done:
765 return (0, 'still waiting')
766 self.__user_done = False
767 return (1, self.__user_answer)
768
769
770
772 msg = _(
773 'Someone tries to forcibly break the existing\n'
774 'controlling connection. This may or may not\n'
775 'have legitimate reasons.\n\n'
776 'Do you want to allow breaking the connection ?'
777 )
778 can_break_conn = gmGuiHelpers.gm_show_question (
779 aMessage = msg,
780 aTitle = _('forced detach attempt')
781 )
782 if can_break_conn:
783 self.__user_answer = 1
784 else:
785 self.__user_answer = 0
786 self.__user_done = True
787 if can_break_conn:
788 self.__pat.locked = False
789 self.__attached = 0
790 return 1
791
793 top_win = wx.GetApp().GetTopWindow()
794 if forced:
795 top_win.Destroy()
796 else:
797 top_win.Close()
798
807
808
809
810 if __name__ == '__main__':
811
812 if len(sys.argv) < 2:
813 sys.exit()
814
815 if sys.argv[1] != 'test':
816 sys.exit()
817
818 gmI18N.activate_locale()
819 gmI18N.install_domain()
820
821
823 handler = gmPlaceholderHandler()
824 handler.debug = True
825
826 for placeholder in ['a', 'b']:
827 print handler[placeholder]
828
829 pat = gmPersonSearch.ask_for_patient()
830 if pat is None:
831 return
832
833 gmPatSearchWidgets.set_active_patient(patient = pat)
834
835 print 'DOB (YYYY-MM-DD):', handler['date_of_birth::%Y-%m-%d']
836
837 app = wx.PyWidgetTester(size = (200, 50))
838 for placeholder in known_placeholders:
839 print placeholder, "=", handler[placeholder]
840
841 ph = 'progress_notes::ap'
842 print '%s: %s' % (ph, handler[ph])
843
845
846 tests = [
847
848 '$<lastname>$',
849 '$<lastname::::3>$',
850 '$<name::%(title)s %(firstnames)s%(preferred)s%(lastnames)s>$',
851
852
853 'lastname',
854 '$<lastname',
855 '$<lastname::',
856 '$<lastname::>$',
857 '$<lastname::abc>$',
858 '$<lastname::abc::>$',
859 '$<lastname::abc::3>$',
860 '$<lastname::abc::xyz>$',
861 '$<lastname::::>$',
862 '$<lastname::::xyz>$',
863
864 '$<date_of_birth::%Y-%m-%d>$',
865 '$<date_of_birth::%Y-%m-%d::3>$',
866 '$<date_of_birth::%Y-%m-%d::>$',
867
868
869 '$<adr_location::home::35>$',
870 '$<gender_mapper::male//female//other::5>$',
871 '$<current_meds::==> %(brand)s %(preparation)s (%(substance)s) <==\n::50>$',
872 '$<allergy_list::%(descriptor)s, >$',
873 '$<current_meds_table::latex//by-brand>$'
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888 ]
889
890 tests = [
891 '$<latest_vaccs_table::latex>$'
892 ]
893
894 pat = gmPersonSearch.ask_for_patient()
895 if pat is None:
896 return
897
898 gmPatSearchWidgets.set_active_patient(patient = pat)
899
900 handler = gmPlaceholderHandler()
901 handler.debug = True
902
903 for placeholder in tests:
904 print placeholder, "=>", handler[placeholder]
905 print "--------------"
906 raw_input()
907
908
909
910
911
912
913
914
915
916
917
919 from Gnumed.pycommon import gmScriptingListener
920 import xmlrpclib
921 listener = gmScriptingListener.cScriptingListener(macro_executor = cMacroPrimitives(personality='unit test'), port=9999)
922
923 s = xmlrpclib.ServerProxy('http://localhost:9999')
924 print "should fail:", s.attach()
925 print "should fail:", s.attach('wrong cookie')
926 print "should work:", s.version()
927 print "should fail:", s.raise_gnumed()
928 print "should fail:", s.raise_notebook_plugin('test plugin')
929 print "should fail:", s.lock_into_patient('kirk, james')
930 print "should fail:", s.unlock_patient()
931 status, conn_auth = s.attach('unit test')
932 print "should work:", status, conn_auth
933 print "should work:", s.version()
934 print "should work:", s.raise_gnumed(conn_auth)
935 status, pat_auth = s.lock_into_patient(conn_auth, 'kirk, james')
936 print "should work:", status, pat_auth
937 print "should fail:", s.unlock_patient(conn_auth, 'bogus patient unlock cookie')
938 print "should work", s.unlock_patient(conn_auth, pat_auth)
939 data = {'firstname': 'jame', 'lastnames': 'Kirk', 'gender': 'm'}
940 status, pat_auth = s.lock_into_patient(conn_auth, data)
941 print "should work:", status, pat_auth
942 print "should work", s.unlock_patient(conn_auth, pat_auth)
943 print s.detach('bogus detach cookie')
944 print s.detach(conn_auth)
945 del s
946
947 listener.shutdown()
948
950
951 import re as regex
952
953 tests = [
954 ' $<lastname>$ ',
955 ' $<lastname::::3>$ ',
956
957
958 '$<date_of_birth::%Y-%m-%d>$',
959 '$<date_of_birth::%Y-%m-%d::3>$',
960 '$<date_of_birth::%Y-%m-%d::>$',
961
962 '$<adr_location::home::35>$',
963 '$<gender_mapper::male//female//other::5>$',
964 '$<current_meds::==> %(brand)s %(preparation)s (%(substance)s) <==\\n::50>$',
965 '$<allergy_list::%(descriptor)s, >$',
966
967 '\\noindent Patient: $<lastname>$, $<firstname>$',
968 '$<allergies::%(descriptor)s & %(l10n_type)s & {\\footnotesize %(reaction)s} \tabularnewline \hline >$',
969 '$<current_meds:: \item[%(substance)s] {\\footnotesize (%(brand)s)} %(preparation)s %(amount)s%(unit)s: %(schedule)s >$'
970 ]
971
972 tests = [
973
974 'junk $<lastname::::3>$ junk',
975 'junk $<lastname::abc::3>$ junk',
976 'junk $<lastname::abc>$ junk',
977 'junk $<lastname>$ junk',
978
979 'junk $<lastname>$ junk $<firstname>$ junk',
980 'junk $<lastname::abc>$ junk $<fiststname::abc>$ junk',
981 'junk $<lastname::abc::3>$ junk $<firstname::abc::3>$ junk',
982 'junk $<lastname::::3>$ junk $<firstname::::3>$ junk'
983
984 ]
985
986 print "testing placeholder regex:", default_placeholder_regex
987 print ""
988
989 for t in tests:
990 print 'line: "%s"' % t
991 print "placeholders:"
992 for p in regex.findall(default_placeholder_regex, t, regex.IGNORECASE):
993 print ' => "%s"' % p
994 print " "
995
1011
1012
1013
1014
1015
1016
1017 test_placeholder()
1018
1019
1020