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