1
2 """GNUmed macro primitives.
3
4 This module implements functions a macro can legally use.
5 """
6
7 __version__ = "$Revision: 1.51 $"
8 __author__ = "K.Hilbert <karsten.hilbert@gmx.net>"
9
10 import sys
11 import time
12 import random
13 import types
14 import logging
15 import os
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
53
54 _log = logging.getLogger('gm.scripting')
55 _cfg = gmCfg2.gmCfgData()
56
57
58 known_placeholders = [
59 'lastname',
60 'firstname',
61 'title',
62 'date_of_birth',
63 'progress_notes',
64 'soap',
65 'soap_s',
66 'soap_o',
67 'soap_a',
68 'soap_p',
69 'soap_u',
70 u'client_version',
71 u'current_provider',
72 u'primary_praxis_provider',
73 u'allergy_state'
74 ]
75
76
77
78
79
80 _injectable_placeholders = {
81 u'form_name_long': None,
82 u'form_name_short': None,
83 u'form_version': None
84 }
85
86
87
88 known_variant_placeholders = [
89
90 u'free_text',
91 u'text_snippet',
92
93 u'data_snippet',
94
95
96
97
98
99
100 u'tex_escape',
101 u'today',
102 u'gender_mapper',
103
104
105
106
107 u'name',
108 u'date_of_birth',
109
110 u'patient_address',
111 u'adr_street',
112 u'adr_number',
113 u'adr_subunit',
114 u'adr_location',
115 u'adr_suburb',
116 u'adr_postcode',
117 u'adr_region',
118 u'adr_country',
119
120 u'patient_comm',
121 u'patient_tags',
122
123
124 u'patient_photo',
125
126
127
128
129
130
131
132 u'external_id',
133
134
135
136 u'soap',
137 u'progress_notes',
138
139
140 u'soap_for_encounters',
141 u'emr_journal',
142
143
144
145
146
147
148
149
150 u'current_meds',
151
152 u'current_meds_table',
153 u'current_meds_notes',
154
155 u'lab_table',
156
157 u'latest_vaccs_table',
158 u'vaccination_history',
159
160 u'allergies',
161 u'allergy_list',
162 u'problems',
163 u'PHX',
164 u'encounter_list',
165
166
167 u'current_provider_external_id',
168 u'primary_praxis_provider_external_id',
169
170
171 u'bill',
172 u'bill_item'
173 ]
174
175
176 default_placeholder_regex = r'\$<.+?>\$'
177
178
179 default_placeholder_start = u'$<'
180 default_placeholder_end = u'>$'
181
183 """Returns values for placeholders.
184
185 - patient related placeholders operate on the currently active patient
186 - is passed to the forms handling code, for example
187
188 Return values when .debug is False:
189 - errors with placeholders return None
190 - placeholders failing to resolve to a value return an empty string
191
192 Return values when .debug is True:
193 - errors with placeholders return an error string
194 - placeholders failing to resolve to a value return a warning string
195
196 There are several types of placeholders:
197
198 simple static placeholders
199 - those are listed in known_placeholders
200 - they are used as-is
201
202 extended static placeholders
203 - those are, effectively, static placeholders
204 with a maximum length attached (after "::::")
205
206 injectable placeholders
207 - they must be set up before use by set_placeholder()
208 - they should be removed after use by unset_placeholder()
209 - the syntax is like extended static placeholders
210 - they are listed in _injectable_placeholders
211
212 variant placeholders
213 - those are listed in known_variant_placeholders
214 - they are parsed into placeholder, data, and maximum length
215 - the length is optional
216 - data is passed to the handler
217
218 Note that this cannot be called from a non-gui thread unless
219 wrapped in wx.CallAfter().
220 """
222
223 self.pat = gmPerson.gmCurrentPatient()
224 self.debug = False
225
226 self.invalid_placeholder_template = _('invalid placeholder [%s]')
227
228 self.__cache = {}
229
230
231
235
239
241 self.__cache[key] = value
242
244 del self.__cache[key]
245
246
247
249 """Map self['placeholder'] to self.placeholder.
250
251 This is useful for replacing placeholders parsed out
252 of documents as strings.
253
254 Unknown/invalid placeholders still deliver a result but
255 it will be glaringly obvious if debugging is enabled.
256 """
257 _log.debug('replacing [%s]', placeholder)
258
259 original_placeholder = placeholder
260
261 if placeholder.startswith(default_placeholder_start):
262 placeholder = placeholder[len(default_placeholder_start):]
263 if placeholder.endswith(default_placeholder_end):
264 placeholder = placeholder[:-len(default_placeholder_end)]
265 else:
266 _log.debug('placeholder must either start with [%s] and end with [%s] or neither of both', default_placeholder_start, default_placeholder_end)
267 if self.debug:
268 return self.invalid_placeholder_template % original_placeholder
269 return None
270
271
272 if placeholder in known_placeholders:
273 return getattr(self, placeholder)
274
275
276 parts = placeholder.split('::::', 1)
277 if len(parts) == 2:
278 name, lng = parts
279 unknown_injectable = False
280 try:
281 val = _injectable_placeholders[name]
282 except KeyError:
283 unknown_injectable = True
284 except:
285 _log.exception('placeholder handling error: %s', original_placeholder)
286 if self.debug:
287 return self.invalid_placeholder_template % original_placeholder
288 return None
289 if not unknown_injectable:
290 if val is None:
291 if self.debug:
292 return u'injectable placeholder [%s]: no value available' % name
293 return placeholder
294 return val[:int(lng)]
295
296
297 parts = placeholder.split('::::', 1)
298 if len(parts) == 2:
299 name, lng = parts
300 try:
301 return getattr(self, name)[:int(lng)]
302 except:
303 _log.exception('placeholder handling error: %s', original_placeholder)
304 if self.debug:
305 return self.invalid_placeholder_template % original_placeholder
306 return None
307
308
309 parts = placeholder.split('::')
310
311 if len(parts) == 1:
312 _log.warning('invalid placeholder layout: %s', original_placeholder)
313 if self.debug:
314 return self.invalid_placeholder_template % original_placeholder
315 return None
316
317 if len(parts) == 2:
318 name, data = parts
319 lng = None
320
321 if len(parts) == 3:
322 name, data, lng = parts
323 try:
324 lng = int(lng)
325 except (TypeError, ValueError):
326 _log.error('placeholder length definition error: %s, discarding length: >%s<', original_placeholder, lng)
327 lng = None
328
329 if len(parts) > 3:
330 _log.warning('invalid placeholder layout: %s', original_placeholder)
331 if self.debug:
332 return self.invalid_placeholder_template % original_placeholder
333 return None
334
335 handler = getattr(self, '_get_variant_%s' % name, None)
336 if handler is None:
337 _log.warning('no handler <_get_variant_%s> for placeholder %s', name, original_placeholder)
338 if self.debug:
339 return self.invalid_placeholder_template % original_placeholder
340 return None
341
342 try:
343 if lng is None:
344 return handler(data = data)
345 return handler(data = data)[:lng]
346 except:
347 _log.exception('placeholder handling error: %s', original_placeholder)
348 if self.debug:
349 return self.invalid_placeholder_template % original_placeholder
350 return None
351
352 _log.error('something went wrong, should never get here')
353 return None
354
355
356
357
358
360 """This does nothing, used as a NOOP properties setter."""
361 pass
362
365
368
371
373 return self._get_variant_date_of_birth(data='%x')
374
376 return self._get_variant_soap()
377
379 return self._get_variant_soap(data = u's')
380
382 return self._get_variant_soap(data = u'o')
383
385 return self._get_variant_soap(data = u'a')
386
388 return self._get_variant_soap(data = u'p')
389
391 return self._get_variant_soap(data = u'u')
392
394 return self._get_variant_soap(soap_cats = None)
395
397 return gmTools.coalesce (
398 _cfg.get(option = u'client_version'),
399 u'%s' % self.__class__.__name__
400 )
401
419
435
437 allg_state = self.pat.get_emr().allergy_state
438
439 if allg_state['last_confirmed'] is None:
440 date_confirmed = u''
441 else:
442 date_confirmed = u' (%s)' % allg_state['last_confirmed'].strftime('%Y %B %d').decode(gmI18N.get_encoding())
443
444 tmp = u'%s%s' % (
445 allg_state.state_string,
446 date_confirmed
447 )
448 return tmp
449
450
451
452 placeholder_regex = property(lambda x: default_placeholder_regex, _setter_noop)
453
454
455
456
457 lastname = property(_get_lastname, _setter_noop)
458 firstname = property(_get_firstname, _setter_noop)
459 title = property(_get_title, _setter_noop)
460 date_of_birth = property(_get_dob, _setter_noop)
461
462 progress_notes = property(_get_progress_notes, _setter_noop)
463 soap = property(_get_progress_notes, _setter_noop)
464 soap_s = property(_get_soap_s, _setter_noop)
465 soap_o = property(_get_soap_o, _setter_noop)
466 soap_a = property(_get_soap_a, _setter_noop)
467 soap_p = property(_get_soap_p, _setter_noop)
468 soap_u = property(_get_soap_u, _setter_noop)
469 soap_admin = property(_get_soap_admin, _setter_noop)
470
471 allergy_state = property(_get_allergy_state, _setter_noop)
472
473 client_version = property(_get_client_version, _setter_noop)
474
475 current_provider = property(_get_current_provider, _setter_noop)
476 primary_praxis_provider = property(_get_primary_praxis_provider, _setter_noop)
477
478
479
481
482 encounters = gmEMRStructWidgets.select_encounters(single_selection = False)
483 if not encounters:
484 return u''
485
486 template = data
487
488 lines = []
489 for enc in encounters:
490 try:
491 lines.append(template % enc)
492 except:
493 lines.append(u'error formatting encounter')
494 _log.exception('problem formatting encounter list')
495 _log.error('template: %s', template)
496 _log.error('encounter: %s', encounter)
497
498 return u'\n'.join(lines)
499
501 """Select encounters from list and format SOAP thereof.
502
503 data: soap_cats (' ' -> None -> admin) // date format
504 """
505
506 cats = None
507 date_format = None
508
509 if data is not None:
510 data_parts = data.split('//')
511
512
513 if len(data_parts[0]) > 0:
514 cats = []
515 if u' ' in data_parts[0]:
516 cats.append(None)
517 data_parts[0] = data_parts[0].replace(u' ', u'')
518 cats.extend(list(data_parts[0]))
519
520
521 if len(data_parts) > 1:
522 if len(data_parts[1]) > 0:
523 date_format = data_parts[1]
524
525 encounters = gmEMRStructWidgets.select_encounters(single_selection = False)
526 if not encounters:
527 return u''
528
529 chunks = []
530 for enc in encounters:
531 chunks.append(enc.format_latex (
532 date_format = date_format,
533 soap_cats = cats,
534 soap_order = u'soap_rank, date'
535 ))
536
537 return u''.join(chunks)
538
540
541 cats = list(u'soapu')
542 cats.append(None)
543 template = u'%s'
544 interactive = True
545 line_length = 9999
546 target_format = None
547 time_range = None
548
549 if data is not None:
550 data_parts = data.split('//')
551
552
553 cats = []
554
555 for c in list(data_parts[0]):
556 if c == u' ':
557 c = None
558 cats.append(c)
559
560 if cats == u'':
561 cats = list(u'soapu').append(None)
562
563
564 if len(data_parts) > 1:
565 template = data_parts[1]
566
567
568 if len(data_parts) > 2:
569 try:
570 line_length = int(data_parts[2])
571 except:
572 line_length = 9999
573
574
575 if len(data_parts) > 3:
576 try:
577 time_range = 7 * int(data_parts[3])
578 except:
579 time_range = None
580
581
582 if len(data_parts) > 4:
583 target_format = data_parts[4]
584
585
586 narr = self.pat.get_emr().get_as_journal(soap_cats = cats, time_range = time_range)
587
588 if len(narr) == 0:
589 return u''
590
591 if target_format == u'tex':
592 keys = narr[0].keys()
593 lines = []
594 line_dict = {}
595 for n in narr:
596 for key in keys:
597 if isinstance(n[key], basestring):
598 line_dict[key] = gmTools.tex_escape_string(text = n[key])
599 continue
600 line_dict[key] = n[key]
601 try:
602 lines.append((template % line_dict)[:line_length])
603 except KeyError:
604 return u'invalid key in template [%s], valid keys: %s]' % (template, str(keys))
605 else:
606 try:
607 lines = [ (template % n)[:line_length] for n in narr ]
608 except KeyError:
609 return u'invalid key in template [%s], valid keys: %s]' % (template, str(narr[0].keys()))
610
611 return u'\n'.join(lines)
612
614 return self._get_variant_soap(data=data)
615
617
618
619 cats = list(u'soapu')
620 cats.append(None)
621 template = u'%s'
622
623 if data is not None:
624 data_parts = data.split('//')
625
626
627 cats = []
628
629 for cat in list(data_parts[0]):
630 if cat == u' ':
631 cat = None
632 cats.append(cat)
633
634 if cats == u'':
635 cats = list(u'soapu')
636 cats.append(None)
637
638
639 if len(data_parts) > 1:
640 template = data_parts[1]
641
642
643 narr = gmNarrativeWidgets.select_narrative_from_episodes(soap_cats = cats)
644
645 if narr is None:
646 return u''
647
648 if len(narr) == 0:
649 return u''
650
651 try:
652 narr = [ template % n['narrative'] for n in narr ]
653 except KeyError:
654 return u'invalid key in template [%s], valid keys: %s]' % (template, str(narr[0].keys()))
655
656 return u'\n'.join(narr)
657
676
679
680
682 values = data.split('//', 2)
683
684 if len(values) == 2:
685 male_value, female_value = values
686 other_value = u'<unkown gender>'
687 elif len(values) == 3:
688 male_value, female_value, other_value = values
689 else:
690 return _('invalid gender mapping layout: [%s]') % data
691
692 if self.pat['gender'] == u'm':
693 return male_value
694
695 if self.pat['gender'] == u'f':
696 return female_value
697
698 return other_value
699
700
701
703
704 data_parts = data.split(u'//')
705
706
707 adr_type = data_parts[0].strip()
708 orig_type = adr_type
709 if adr_type != u'':
710 adrs = self.pat.get_addresses(address_type = adr_type)
711 if len(adrs) == 0:
712 _log.warning('no address for type [%s]', adr_type)
713 adr_type = u''
714 if adr_type == u'':
715 _log.debug('asking user for address type')
716 adr = gmPersonContactWidgets.select_address(missing = orig_type, person = self.pat)
717 if adr is None:
718 if self.debug:
719 return _('no address type replacement selected')
720 return u''
721 adr_type = adr['address_type']
722 adr = self.pat.get_addresses(address_type = adr_type)[0]
723
724
725 template = _('%(street)s %(number)s, %(postcode)s %(urb)s, %(l10n_state)s, %(l10n_country)s')
726 if len(data_parts) > 1:
727 if data_parts[1].strip() != u'':
728 template = data_parts[1]
729
730 try:
731 return template % adr.fields_as_dict()
732 except StandardError:
733 _log.exception('error formatting address')
734 _log.error('template: %s', template)
735
736 return None
737
739 requested_type = data.strip()
740 cache_key = 'adr-type-%s' % requested_type
741 try:
742 type2use = self.__cache[cache_key]
743 _log.debug('cache hit (%s): [%s] -> [%s]', cache_key, requested_type, type2use)
744 except KeyError:
745 type2use = requested_type
746 if type2use != u'':
747 adrs = self.pat.get_addresses(address_type = type2use)
748 if len(adrs) == 0:
749 _log.warning('no address of type [%s] for <%s> field extraction', requested_type, part)
750 type2use = u''
751 if type2use == u'':
752 _log.debug('asking user for replacement address type')
753 adr = gmPersonContactWidgets.select_address(missing = requested_type, person = self.pat)
754 if adr is None:
755 _log.debug('no replacement selected')
756 if self.debug:
757 return _('no address type replacement selected')
758 return u''
759 type2use = adr['address_type']
760 self.__cache[cache_key] = type2use
761 _log.debug('caching (%s): [%s] -> [%s]', cache_key, requested_type, type2use)
762
763 return self.pat.get_addresses(address_type = type2use)[0][part]
764
766 return self.__get_variant_adr_part(data = data, part = 'street')
767
769 return self.__get_variant_adr_part(data = data, part = 'number')
770
772 return self.__get_variant_adr_part(data = data, part = 'subunit')
773
775 return self.__get_variant_adr_part(data = data, part = 'urb')
776
778 return self.__get_variant_adr_part(data = data, part = 'suburb')
779
780 - def _get_variant_adr_postcode(self, data=u'?'):
781 return self.__get_variant_adr_part(data = data, part = 'postcode')
782
784 return self.__get_variant_adr_part(data = data, part = 'l10n_state')
785
787 return self.__get_variant_adr_part(data = data, part = 'l10n_country')
788
790 comms = self.pat.get_comm_channels(comm_medium = data)
791 if len(comms) == 0:
792 if self.debug:
793 return _('no URL for comm channel [%s]') % data
794 return u''
795 return comms[0]['url']
796
798
799 template = u'%s'
800 target_mime = None
801 target_ext = None
802 if data is not None:
803 parts = data.split(u'//')
804 template = parts[0]
805 if len(parts) > 1:
806 target_mime = parts[1].strip()
807 if len(parts) > 2:
808 target_ext = parts[2].strip()
809 if target_ext is None:
810 if target_mime is not None:
811 target_ext = gmMimeLib.guess_ext_by_mimetype(mimetype = target_mime)
812
813 mugshot = self.pat.document_folder.latest_mugshot
814 if mugshot is None:
815 if self.debug:
816 return _('no mugshot available')
817 return u''
818
819 fname = mugshot.export_to_file (
820 target_mime = target_mime,
821 target_extension = target_ext,
822 ignore_conversion_problems = True
823 )
824 if fname is None:
825 if self.debug:
826 return _('cannot export or convert latest mugshot')
827 return u''
828
829 return template % fname
830
847
848
849
850
852 data_parts = data.split(u'//')
853 if len(data_parts) < 2:
854 return u'current provider external ID: template is missing'
855
856 id_type = data_parts[0].strip()
857 if id_type == u'':
858 return u'current provider external ID: type is missing'
859
860 issuer = data_parts[1].strip()
861 if issuer == u'':
862 return u'current provider external ID: issuer is missing'
863
864 prov = gmStaff.gmCurrentProvider()
865 ids = prov.identity.get_external_ids(id_type = id_type, issuer = issuer)
866
867 if len(ids) == 0:
868 if self.debug:
869 return _('no external ID [%s] by [%s]') % (id_type, issuer)
870 return u''
871
872 return ids[0]['value']
873
875 data_parts = data.split(u'//')
876 if len(data_parts) < 2:
877 return u'primary in-praxis provider external ID: template is missing'
878
879 id_type = data_parts[0].strip()
880 if id_type == u'':
881 return u'primary in-praxis provider external ID: type is missing'
882
883 issuer = data_parts[1].strip()
884 if issuer == u'':
885 return u'primary in-praxis provider external ID: issuer is missing'
886
887 prov = self.pat.primary_provider
888 if prov is None:
889 if self.debug:
890 return _('no primary in-praxis provider')
891 return u''
892
893 ids = prov.identity.get_external_ids(id_type = id_type, issuer = issuer)
894
895 if len(ids) == 0:
896 if self.debug:
897 return _('no external ID [%s] by [%s]') % (id_type, issuer)
898 return u''
899
900 return ids[0]['value']
901
903 data_parts = data.split(u'//')
904 if len(data_parts) < 2:
905 return u'patient external ID: template is missing'
906
907 id_type = data_parts[0].strip()
908 if id_type == u'':
909 return u'patient external ID: type is missing'
910
911 issuer = data_parts[1].strip()
912 if issuer == u'':
913 return u'patient external ID: issuer is missing'
914
915 ids = self.pat.get_external_ids(id_type = id_type, issuer = issuer)
916
917 if len(ids) == 0:
918 if self.debug:
919 return _('no external ID [%s] by [%s]') % (id_type, issuer)
920 return u''
921
922 return ids[0]['value']
923
925 if data is None:
926 return [_('template is missing')]
927
928 template, separator = data.split('//', 2)
929
930 emr = self.pat.get_emr()
931 return separator.join([ template % a for a in emr.get_allergies() ])
932
940
968
970
971 options = data.split('//')
972
973 if u'latex' in options:
974 return gmMedication.format_substance_intake (
975 emr = self.pat.get_emr(),
976 output_format = u'latex',
977 table_type = u'by-brand'
978 )
979
980 _log.error('no known current medications table formatting style in [%s]', data)
981 return _('unknown current medication table formatting style')
982
984
985 options = data.split('//')
986
987 if u'latex' in options:
988 return gmMedication.format_substance_intake_notes (
989 emr = self.pat.get_emr(),
990 output_format = u'latex',
991 table_type = u'by-brand'
992 )
993
994 _log.error('no known current medications notes formatting style in [%s]', data)
995 return _('unknown current medication notes formatting style')
996
1011
1023
1025 options = data.split('//')
1026 template = options[0]
1027 if len(options) > 1:
1028 date_format = options[1]
1029 else:
1030 date_format = u'%Y %B %d'
1031
1032 emr = self.pat.get_emr()
1033 vaccs = emr.get_vaccinations(order_by = u'date_given DESC, vaccine')
1034
1035 return u'\n'.join([ template % v.fields_as_dict(date_format = date_format) for v in vaccs ])
1036
1038
1039 if data is None:
1040 if self.debug:
1041 _log.error('PHX: missing placeholder arguments')
1042 return _('PHX: Invalid placeholder options.')
1043 return u''
1044
1045 _log.debug('arguments: %s', data)
1046
1047 data_parts = data.split(u'//')
1048 template = u'%s'
1049 separator = u'\n'
1050 date_format = '%Y %B %d'
1051 esc_style = None
1052 try:
1053 template = data_parts[0]
1054 separator = data_parts[1]
1055 date_format = data_parts[2]
1056 esc_style = data_parts[3]
1057 except IndexError:
1058 pass
1059
1060 phxs = gmEMRStructWidgets.select_health_issues(emr = self.pat.emr)
1061 if phxs is None:
1062 if self.debug:
1063 return _('no PHX for this patient (available or selected)')
1064 return u''
1065
1066 return separator.join ([
1067 template % phx.fields_as_dict (
1068 date_format = date_format,
1069 escape_style = esc_style,
1070 bool_strings = (_('yes'), _('no'))
1071 ) for phx in phxs
1072 ])
1073
1075
1076 if data is None:
1077 return [_('template is missing')]
1078
1079 probs = self.pat.get_emr().get_problems()
1080
1081 return u'\n'.join([ data % p for p in probs ])
1082
1085
1088
1089 - def _get_variant_text_snippet(self, data=None):
1090 data_parts = data.split(u'//')
1091 keyword = data_parts[0]
1092 template = u'%s'
1093 if len(data_parts) > 1:
1094 template = data_parts[1]
1095
1096 expansion = gmKeywordExpansion.get_expansion (
1097 keyword = keyword,
1098 textual_only = True,
1099 binary_only = False
1100 )
1101 if expansion is None:
1102 if self.debug:
1103 return _('no textual expansion found for keyword <%s>' % keyword)
1104 return u''
1105
1106
1107 return template % expansion['expansion']
1108
1164
1165 - def _get_variant_free_text(self, data=u'tex//'):
1166
1167
1168
1169
1170 data_parts = data.split('//')
1171 format = data_parts[0]
1172 if len(data_parts) > 1:
1173 msg = data_parts[1]
1174 else:
1175 msg = _('generic text')
1176
1177 dlg = gmGuiHelpers.cMultilineTextEntryDlg (
1178 None,
1179 -1,
1180 title = _('Replacing <free_text> placeholder'),
1181 msg = _('Below you can enter free text.\n\n [%s]') % msg
1182 )
1183 dlg.enable_user_formatting = True
1184 decision = dlg.ShowModal()
1185
1186 if decision != wx.ID_SAVE:
1187 dlg.Destroy()
1188 if self.debug:
1189 return _('Text input cancelled by user.')
1190 return u''
1191
1192 text = dlg.value.strip()
1193 if dlg.is_user_formatted:
1194 dlg.Destroy()
1195 return text
1196
1197 dlg.Destroy()
1198
1199 if format == u'tex':
1200 return gmTools.tex_escape_string(text = text)
1201
1202 return text
1203
1217
1231
1232
1233
1236
1238 """Functions a macro can legally use.
1239
1240 An instance of this class is passed to the GNUmed scripting
1241 listener. Hence, all actions a macro can legally take must
1242 be defined in this class. Thus we achieve some screening for
1243 security and also thread safety handling.
1244 """
1245
1246 - def __init__(self, personality = None):
1247 if personality is None:
1248 raise gmExceptions.ConstructorError, 'must specify personality'
1249 self.__personality = personality
1250 self.__attached = 0
1251 self._get_source_personality = None
1252 self.__user_done = False
1253 self.__user_answer = 'no answer yet'
1254 self.__pat = gmPerson.gmCurrentPatient()
1255
1256 self.__auth_cookie = str(random.random())
1257 self.__pat_lock_cookie = str(random.random())
1258 self.__lock_after_load_cookie = str(random.random())
1259
1260 _log.info('slave mode personality is [%s]', personality)
1261
1262
1263
1264 - def attach(self, personality = None):
1265 if self.__attached:
1266 _log.error('attach with [%s] rejected, already serving a client', personality)
1267 return (0, _('attach rejected, already serving a client'))
1268 if personality != self.__personality:
1269 _log.error('rejecting attach to personality [%s], only servicing [%s]' % (personality, self.__personality))
1270 return (0, _('attach to personality [%s] rejected') % personality)
1271 self.__attached = 1
1272 self.__auth_cookie = str(random.random())
1273 return (1, self.__auth_cookie)
1274
1275 - def detach(self, auth_cookie=None):
1276 if not self.__attached:
1277 return 1
1278 if auth_cookie != self.__auth_cookie:
1279 _log.error('rejecting detach() with cookie [%s]' % auth_cookie)
1280 return 0
1281 self.__attached = 0
1282 return 1
1283
1285 if not self.__attached:
1286 return 1
1287 self.__user_done = False
1288
1289 wx.CallAfter(self._force_detach)
1290 return 1
1291
1293 ver = _cfg.get(option = u'client_version')
1294 return "GNUmed %s, %s $Revision: 1.51 $" % (ver, self.__class__.__name__)
1295
1297 """Shuts down this client instance."""
1298 if not self.__attached:
1299 return 0
1300 if auth_cookie != self.__auth_cookie:
1301 _log.error('non-authenticated shutdown_gnumed()')
1302 return 0
1303 wx.CallAfter(self._shutdown_gnumed, forced)
1304 return 1
1305
1307 """Raise ourselves to the top of the desktop."""
1308 if not self.__attached:
1309 return 0
1310 if auth_cookie != self.__auth_cookie:
1311 _log.error('non-authenticated raise_gnumed()')
1312 return 0
1313 return "cMacroPrimitives.raise_gnumed() not implemented"
1314
1316 if not self.__attached:
1317 return 0
1318 if auth_cookie != self.__auth_cookie:
1319 _log.error('non-authenticated get_loaded_plugins()')
1320 return 0
1321 gb = gmGuiBroker.GuiBroker()
1322 return gb['horstspace.notebook.gui'].keys()
1323
1325 """Raise a notebook plugin within GNUmed."""
1326 if not self.__attached:
1327 return 0
1328 if auth_cookie != self.__auth_cookie:
1329 _log.error('non-authenticated raise_notebook_plugin()')
1330 return 0
1331
1332 wx.CallAfter(gmPlugin.raise_notebook_plugin, a_plugin)
1333 return 1
1334
1336 """Load external patient, perhaps create it.
1337
1338 Callers must use get_user_answer() to get status information.
1339 It is unsafe to proceed without knowing the completion state as
1340 the controlled client may be waiting for user input from a
1341 patient selection list.
1342 """
1343 if not self.__attached:
1344 return (0, _('request rejected, you are not attach()ed'))
1345 if auth_cookie != self.__auth_cookie:
1346 _log.error('non-authenticated load_patient_from_external_source()')
1347 return (0, _('rejected load_patient_from_external_source(), not authenticated'))
1348 if self.__pat.locked:
1349 _log.error('patient is locked, cannot load from external source')
1350 return (0, _('current patient is locked'))
1351 self.__user_done = False
1352 wx.CallAfter(self._load_patient_from_external_source)
1353 self.__lock_after_load_cookie = str(random.random())
1354 return (1, self.__lock_after_load_cookie)
1355
1357 if not self.__attached:
1358 return (0, _('request rejected, you are not attach()ed'))
1359 if auth_cookie != self.__auth_cookie:
1360 _log.error('non-authenticated lock_load_patient()')
1361 return (0, _('rejected lock_load_patient(), not authenticated'))
1362
1363 if lock_after_load_cookie != self.__lock_after_load_cookie:
1364 _log.warning('patient lock-after-load request rejected due to wrong cookie [%s]' % lock_after_load_cookie)
1365 return (0, 'patient lock-after-load request rejected, wrong cookie provided')
1366 self.__pat.locked = True
1367 self.__pat_lock_cookie = str(random.random())
1368 return (1, self.__pat_lock_cookie)
1369
1371 if not self.__attached:
1372 return (0, _('request rejected, you are not attach()ed'))
1373 if auth_cookie != self.__auth_cookie:
1374 _log.error('non-authenticated lock_into_patient()')
1375 return (0, _('rejected lock_into_patient(), not authenticated'))
1376 if self.__pat.locked:
1377 _log.error('patient is already locked')
1378 return (0, _('already locked into a patient'))
1379 searcher = gmPersonSearch.cPatientSearcher_SQL()
1380 if type(search_params) == types.DictType:
1381 idents = searcher.get_identities(search_dict=search_params)
1382 raise StandardError("must use dto, not search_dict")
1383 else:
1384 idents = searcher.get_identities(search_term=search_params)
1385 if idents is None:
1386 return (0, _('error searching for patient with [%s]/%s') % (search_term, search_dict))
1387 if len(idents) == 0:
1388 return (0, _('no patient found for [%s]/%s') % (search_term, search_dict))
1389
1390 if len(idents) > 1:
1391 return (0, _('several matching patients found for [%s]/%s') % (search_term, search_dict))
1392 if not gmPatSearchWidgets.set_active_patient(patient = idents[0]):
1393 return (0, _('cannot activate patient [%s] (%s/%s)') % (str(idents[0]), search_term, search_dict))
1394 self.__pat.locked = True
1395 self.__pat_lock_cookie = str(random.random())
1396 return (1, self.__pat_lock_cookie)
1397
1399 if not self.__attached:
1400 return (0, _('request rejected, you are not attach()ed'))
1401 if auth_cookie != self.__auth_cookie:
1402 _log.error('non-authenticated unlock_patient()')
1403 return (0, _('rejected unlock_patient, not authenticated'))
1404
1405 if not self.__pat.locked:
1406 return (1, '')
1407
1408 if unlock_cookie != self.__pat_lock_cookie:
1409 _log.warning('patient unlock request rejected due to wrong cookie [%s]' % unlock_cookie)
1410 return (0, 'patient unlock request rejected, wrong cookie provided')
1411 self.__pat.locked = False
1412 return (1, '')
1413
1415 if not self.__attached:
1416 return 0
1417 if auth_cookie != self.__auth_cookie:
1418 _log.error('non-authenticated select_identity()')
1419 return 0
1420 return "cMacroPrimitives.assume_staff_identity() not implemented"
1421
1423 if not self.__user_done:
1424 return (0, 'still waiting')
1425 self.__user_done = False
1426 return (1, self.__user_answer)
1427
1428
1429
1431 msg = _(
1432 'Someone tries to forcibly break the existing\n'
1433 'controlling connection. This may or may not\n'
1434 'have legitimate reasons.\n\n'
1435 'Do you want to allow breaking the connection ?'
1436 )
1437 can_break_conn = gmGuiHelpers.gm_show_question (
1438 aMessage = msg,
1439 aTitle = _('forced detach attempt')
1440 )
1441 if can_break_conn:
1442 self.__user_answer = 1
1443 else:
1444 self.__user_answer = 0
1445 self.__user_done = True
1446 if can_break_conn:
1447 self.__pat.locked = False
1448 self.__attached = 0
1449 return 1
1450
1452 top_win = wx.GetApp().GetTopWindow()
1453 if forced:
1454 top_win.Destroy()
1455 else:
1456 top_win.Close()
1457
1466
1467
1468
1469 if __name__ == '__main__':
1470
1471 if len(sys.argv) < 2:
1472 sys.exit()
1473
1474 if sys.argv[1] != 'test':
1475 sys.exit()
1476
1477 gmI18N.activate_locale()
1478 gmI18N.install_domain()
1479
1480
1482 handler = gmPlaceholderHandler()
1483 handler.debug = True
1484
1485 for placeholder in ['a', 'b']:
1486 print handler[placeholder]
1487
1488 pat = gmPersonSearch.ask_for_patient()
1489 if pat is None:
1490 return
1491
1492 gmPatSearchWidgets.set_active_patient(patient = pat)
1493
1494 print 'DOB (YYYY-MM-DD):', handler['date_of_birth::%Y-%m-%d']
1495
1496 app = wx.PyWidgetTester(size = (200, 50))
1497 for placeholder in known_placeholders:
1498 print placeholder, "=", handler[placeholder]
1499
1500 ph = 'progress_notes::ap'
1501 print '%s: %s' % (ph, handler[ph])
1502
1504
1505 tests = [
1506
1507 '$<lastname>$',
1508 '$<lastname::::3>$',
1509 '$<name::%(title)s %(firstnames)s%(preferred)s%(lastnames)s>$',
1510
1511
1512 'lastname',
1513 '$<lastname',
1514 '$<lastname::',
1515 '$<lastname::>$',
1516 '$<lastname::abc>$',
1517 '$<lastname::abc::>$',
1518 '$<lastname::abc::3>$',
1519 '$<lastname::abc::xyz>$',
1520 '$<lastname::::>$',
1521 '$<lastname::::xyz>$',
1522
1523 '$<date_of_birth::%Y-%m-%d>$',
1524 '$<date_of_birth::%Y-%m-%d::3>$',
1525 '$<date_of_birth::%Y-%m-%d::>$',
1526
1527
1528 '$<adr_location::home::35>$',
1529 '$<gender_mapper::male//female//other::5>$',
1530 '$<current_meds::==> %(brand)s %(preparation)s (%(substance)s) <==\n::50>$',
1531 '$<allergy_list::%(descriptor)s, >$',
1532 '$<current_meds_table::latex//by-brand>$'
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547 ]
1548
1549
1550
1551
1552
1553 pat = gmPersonSearch.ask_for_patient()
1554 if pat is None:
1555 return
1556
1557 gmPatSearchWidgets.set_active_patient(patient = pat)
1558
1559 handler = gmPlaceholderHandler()
1560 handler.debug = True
1561
1562 for placeholder in tests:
1563 print placeholder, "=>", handler[placeholder]
1564 print "--------------"
1565 raw_input()
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1578 from Gnumed.pycommon import gmScriptingListener
1579 import xmlrpclib
1580 listener = gmScriptingListener.cScriptingListener(macro_executor = cMacroPrimitives(personality='unit test'), port=9999)
1581
1582 s = xmlrpclib.ServerProxy('http://localhost:9999')
1583 print "should fail:", s.attach()
1584 print "should fail:", s.attach('wrong cookie')
1585 print "should work:", s.version()
1586 print "should fail:", s.raise_gnumed()
1587 print "should fail:", s.raise_notebook_plugin('test plugin')
1588 print "should fail:", s.lock_into_patient('kirk, james')
1589 print "should fail:", s.unlock_patient()
1590 status, conn_auth = s.attach('unit test')
1591 print "should work:", status, conn_auth
1592 print "should work:", s.version()
1593 print "should work:", s.raise_gnumed(conn_auth)
1594 status, pat_auth = s.lock_into_patient(conn_auth, 'kirk, james')
1595 print "should work:", status, pat_auth
1596 print "should fail:", s.unlock_patient(conn_auth, 'bogus patient unlock cookie')
1597 print "should work", s.unlock_patient(conn_auth, pat_auth)
1598 data = {'firstname': 'jame', 'lastnames': 'Kirk', 'gender': 'm'}
1599 status, pat_auth = s.lock_into_patient(conn_auth, data)
1600 print "should work:", status, pat_auth
1601 print "should work", s.unlock_patient(conn_auth, pat_auth)
1602 print s.detach('bogus detach cookie')
1603 print s.detach(conn_auth)
1604 del s
1605
1606 listener.shutdown()
1607
1609
1610 import re as regex
1611
1612 tests = [
1613 ' $<lastname>$ ',
1614 ' $<lastname::::3>$ ',
1615
1616
1617 '$<date_of_birth::%Y-%m-%d>$',
1618 '$<date_of_birth::%Y-%m-%d::3>$',
1619 '$<date_of_birth::%Y-%m-%d::>$',
1620
1621 '$<adr_location::home::35>$',
1622 '$<gender_mapper::male//female//other::5>$',
1623 '$<current_meds::==> %(brand)s %(preparation)s (%(substance)s) <==\\n::50>$',
1624 '$<allergy_list::%(descriptor)s, >$',
1625
1626 '\\noindent Patient: $<lastname>$, $<firstname>$',
1627 '$<allergies::%(descriptor)s & %(l10n_type)s & {\\footnotesize %(reaction)s} \tabularnewline \hline >$',
1628 '$<current_meds:: \item[%(substance)s] {\\footnotesize (%(brand)s)} %(preparation)s %(amount)s%(unit)s: %(schedule)s >$'
1629 ]
1630
1631 tests = [
1632
1633 'junk $<lastname::::3>$ junk',
1634 'junk $<lastname::abc::3>$ junk',
1635 'junk $<lastname::abc>$ junk',
1636 'junk $<lastname>$ junk',
1637
1638 'junk $<lastname>$ junk $<firstname>$ junk',
1639 'junk $<lastname::abc>$ junk $<fiststname::abc>$ junk',
1640 'junk $<lastname::abc::3>$ junk $<firstname::abc::3>$ junk',
1641 'junk $<lastname::::3>$ junk $<firstname::::3>$ junk'
1642
1643 ]
1644
1645 print "testing placeholder regex:", default_placeholder_regex
1646 print ""
1647
1648 for t in tests:
1649 print 'line: "%s"' % t
1650 print "placeholders:"
1651 for p in regex.findall(default_placeholder_regex, t, regex.IGNORECASE):
1652 print ' => "%s"' % p
1653 print " "
1654
1708
1709
1717
1718
1719
1720
1721
1722
1723
1724 test_placeholder()
1725
1726
1727
1728