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, time, random, types, logging
11
12
13 import wx
14
15
16 if __name__ == '__main__':
17 sys.path.insert(0, '../../')
18 from Gnumed.pycommon import gmI18N
19 if __name__ == '__main__':
20 gmI18N.activate_locale()
21 gmI18N.install_domain()
22 from Gnumed.pycommon import gmGuiBroker
23 from Gnumed.pycommon import gmTools
24 from Gnumed.pycommon import gmBorg
25 from Gnumed.pycommon import gmExceptions
26 from Gnumed.pycommon import gmCfg2
27 from Gnumed.pycommon import gmDateTime
28 from Gnumed.pycommon import gmMimeLib
29
30 from Gnumed.business import gmPerson
31 from Gnumed.business import gmStaff
32 from Gnumed.business import gmDemographicRecord
33 from Gnumed.business import gmMedication
34 from Gnumed.business import gmPathLab
35 from Gnumed.business import gmPersonSearch
36 from Gnumed.business import gmVaccination
37 from Gnumed.business import gmPersonSearch
38 from Gnumed.business import gmKeywordExpansion
39
40 from Gnumed.wxpython import gmGuiHelpers
41 from Gnumed.wxpython import gmNarrativeWidgets
42 from Gnumed.wxpython import gmPatSearchWidgets
43 from Gnumed.wxpython import gmPersonContactWidgets
44 from Gnumed.wxpython import gmPlugin
45 from Gnumed.wxpython import gmEMRStructWidgets
46 from Gnumed.wxpython import gmListWidgets
47 from Gnumed.wxpython import gmDemographicsWidgets
48
49
50 _log = logging.getLogger('gm.scripting')
51 _cfg = gmCfg2.gmCfgData()
52
53
54 known_placeholders = [
55 'lastname',
56 'firstname',
57 'title',
58 'date_of_birth',
59 'progress_notes',
60 'soap',
61 'soap_s',
62 'soap_o',
63 'soap_a',
64 'soap_p',
65 'soap_u',
66 u'client_version',
67 u'current_provider',
68 u'primary_praxis_provider',
69 u'allergy_state'
70 ]
71
72
73
74
75
76 _injectable_placeholders = {
77 u'form_name_long': None,
78 u'form_name_short': None,
79 u'form_version': None
80 }
81
82
83
84 known_variant_placeholders = [
85
86 u'free_text',
87 u'text_snippet',
88
89 u'data_snippet',
90
91
92
93
94
95
96 u'tex_escape',
97 u'today',
98 u'gender_mapper',
99
100
101
102
103 u'date_of_birth',
104
105 u'patient_address',
106 u'adr_street',
107 u'adr_number',
108 u'adr_subunit',
109 u'adr_location',
110 u'adr_suburb',
111 u'adr_postcode',
112 u'adr_region',
113 u'adr_country',
114
115 u'patient_comm',
116 u'patient_tags',
117
118
119 u'patient_photo',
120
121
122
123
124
125
126
127 u'external_id',
128
129
130 u'current_meds',
131 u'current_meds_table',
132
133
134 u'soap',
135 u'progress_notes',
136
137
138 u'emr_journal',
139
140
141
142
143
144
145
146 u'current_meds_notes',
147 u'lab_table',
148 u'latest_vaccs_table',
149 u'vaccination_history',
150 u'allergies',
151 u'allergy_list',
152 u'problems',
153 u'PHX',
154 u'name',
155 u'soap_for_encounters',
156 u'encounter_list',
157
158
159 u'current_provider_external_id',
160 u'primary_praxis_provider_external_id',
161
162
163 u'bill',
164 u'bill_item'
165 ]
166
167
168 default_placeholder_regex = r'\$<.+?>\$'
169
170
171 default_placeholder_start = u'$<'
172 default_placeholder_end = u'>$'
173
175 """Returns values for placeholders.
176
177 - patient related placeholders operate on the currently active patient
178 - is passed to the forms handling code, for example
179
180 Return values when .debug is False:
181 - errors with placeholders return None
182 - placeholders failing to resolve to a value return an empty string
183
184 Return values when .debug is True:
185 - errors with placeholders return an error string
186 - placeholders failing to resolve to a value return a warning string
187
188 There are several types of placeholders:
189
190 simple static placeholders
191 - those are listed in known_placeholders
192 - they are used as-is
193
194 extended static placeholders
195 - those are, effectively, static placeholders
196 with a maximum length attached (after "::::")
197
198 injectable placeholders
199 - they must be set up before use by set_placeholder()
200 - they should be removed after use by unset_placeholder()
201 - the syntax is like extended static placeholders
202 - they are listed in _injectable_placeholders
203
204 variant placeholders
205 - those are listed in known_variant_placeholders
206 - they are parsed into placeholder, data, and maximum length
207 - the length is optional
208 - data is passed to the handler
209
210 Note that this cannot be called from a non-gui thread unless
211 wrapped in wx.CallAfter().
212 """
214
215 self.pat = gmPerson.gmCurrentPatient()
216 self.debug = False
217
218 self.invalid_placeholder_template = _('invalid placeholder [%s]')
219
220 self.__cache = {}
221
222
223
227
231
233 self.__cache[key] = value
234
236 del self.__cache[key]
237
238
239
241 """Map self['placeholder'] to self.placeholder.
242
243 This is useful for replacing placeholders parsed out
244 of documents as strings.
245
246 Unknown/invalid placeholders still deliver a result but
247 it will be glaringly obvious if debugging is enabled.
248 """
249 _log.debug('replacing [%s]', placeholder)
250
251 original_placeholder = placeholder
252
253 if placeholder.startswith(default_placeholder_start):
254 placeholder = placeholder[len(default_placeholder_start):]
255 if placeholder.endswith(default_placeholder_end):
256 placeholder = placeholder[:-len(default_placeholder_end)]
257 else:
258 _log.debug('placeholder must either start with [%s] and end with [%s] or neither of both', default_placeholder_start, default_placeholder_end)
259 if self.debug:
260 return self.invalid_placeholder_template % original_placeholder
261 return None
262
263
264 if placeholder in known_placeholders:
265 return getattr(self, placeholder)
266
267
268 parts = placeholder.split('::::', 1)
269 if len(parts) == 2:
270 name, lng = parts
271 unknown_injectable = False
272 try:
273 val = _injectable_placeholders[name]
274 except KeyError:
275 unknown_injectable = True
276 except:
277 _log.exception('placeholder handling error: %s', original_placeholder)
278 if self.debug:
279 return self.invalid_placeholder_template % original_placeholder
280 return None
281 if not unknown_injectable:
282 if val is None:
283 if self.debug:
284 return u'injectable placeholder [%s]: no value available' % name
285 return placeholder
286 return val[:int(lng)]
287
288
289 parts = placeholder.split('::::', 1)
290 if len(parts) == 2:
291 name, lng = parts
292 try:
293 return getattr(self, name)[:int(lng)]
294 except:
295 _log.exception('placeholder handling error: %s', original_placeholder)
296 if self.debug:
297 return self.invalid_placeholder_template % original_placeholder
298 return None
299
300
301 parts = placeholder.split('::')
302
303 if len(parts) == 1:
304 _log.warning('invalid placeholder layout: %s', original_placeholder)
305 if self.debug:
306 return self.invalid_placeholder_template % original_placeholder
307 return None
308
309 if len(parts) == 2:
310 name, data = parts
311 lng = None
312
313 if len(parts) == 3:
314 name, data, lng = parts
315 try:
316 lng = int(lng)
317 except (TypeError, ValueError):
318 _log.error('placeholder length definition error: %s, discarding length: >%s<', original_placeholder, lng)
319 lng = None
320
321 if len(parts) > 3:
322 _log.warning('invalid placeholder layout: %s', original_placeholder)
323 if self.debug:
324 return self.invalid_placeholder_template % original_placeholder
325 return None
326
327 handler = getattr(self, '_get_variant_%s' % name, None)
328 if handler is None:
329 _log.warning('no handler <_get_variant_%s> for placeholder %s', name, original_placeholder)
330 if self.debug:
331 return self.invalid_placeholder_template % original_placeholder
332 return None
333
334 try:
335 if lng is None:
336 return handler(data = data)
337 return handler(data = data)[:lng]
338 except:
339 _log.exception('placeholder handling error: %s', original_placeholder)
340 if self.debug:
341 return self.invalid_placeholder_template % original_placeholder
342 return None
343
344 _log.error('something went wrong, should never get here')
345 return None
346
347
348
349
350
352 """This does nothing, used as a NOOP properties setter."""
353 pass
354
357
360
363
365 return self._get_variant_date_of_birth(data='%x')
366
368 return self._get_variant_soap()
369
371 return self._get_variant_soap(data = u's')
372
374 return self._get_variant_soap(data = u'o')
375
377 return self._get_variant_soap(data = u'a')
378
380 return self._get_variant_soap(data = u'p')
381
383 return self._get_variant_soap(data = u'u')
384
386 return self._get_variant_soap(soap_cats = None)
387
389 return gmTools.coalesce (
390 _cfg.get(option = u'client_version'),
391 u'%s' % self.__class__.__name__
392 )
393
411
427
429 allg_state = self.pat.get_emr().allergy_state
430
431 if allg_state['last_confirmed'] is None:
432 date_confirmed = u''
433 else:
434 date_confirmed = u' (%s)' % allg_state['last_confirmed'].strftime('%Y %B %d').decode(gmI18N.get_encoding())
435
436 tmp = u'%s%s' % (
437 allg_state.state_string,
438 date_confirmed
439 )
440 return tmp
441
442
443
444 placeholder_regex = property(lambda x: default_placeholder_regex, _setter_noop)
445
446
447
448
449 lastname = property(_get_lastname, _setter_noop)
450 firstname = property(_get_firstname, _setter_noop)
451 title = property(_get_title, _setter_noop)
452 date_of_birth = property(_get_dob, _setter_noop)
453
454 progress_notes = property(_get_progress_notes, _setter_noop)
455 soap = property(_get_progress_notes, _setter_noop)
456 soap_s = property(_get_soap_s, _setter_noop)
457 soap_o = property(_get_soap_o, _setter_noop)
458 soap_a = property(_get_soap_a, _setter_noop)
459 soap_p = property(_get_soap_p, _setter_noop)
460 soap_u = property(_get_soap_u, _setter_noop)
461 soap_admin = property(_get_soap_admin, _setter_noop)
462
463 allergy_state = property(_get_allergy_state, _setter_noop)
464
465 client_version = property(_get_client_version, _setter_noop)
466
467 current_provider = property(_get_current_provider, _setter_noop)
468 primary_praxis_provider = property(_get_primary_praxis_provider, _setter_noop)
469
470
471
473
474 encounters = gmEMRStructWidgets.select_encounters(single_selection = False)
475 if not encounters:
476 return u''
477
478 template = data
479
480 lines = []
481 for enc in encounters:
482 try:
483 lines.append(template % enc)
484 except:
485 lines.append(u'error formatting encounter')
486 _log.exception('problem formatting encounter list')
487 _log.error('template: %s', template)
488 _log.error('encounter: %s', encounter)
489
490 return u'\n'.join(lines)
491
493 """Select encounters from list and format SOAP thereof.
494
495 data: soap_cats (' ' -> None -> admin) // date format
496 """
497
498 cats = None
499 date_format = None
500
501 if data is not None:
502 data_parts = data.split('//')
503
504
505 if len(data_parts[0]) > 0:
506 cats = []
507 if u' ' in data_parts[0]:
508 cats.append(None)
509 data_parts[0] = data_parts[0].replace(u' ', u'')
510 cats.extend(list(data_parts[0]))
511
512
513 if len(data_parts) > 1:
514 if len(data_parts[1]) > 0:
515 date_format = data_parts[1]
516
517 encounters = gmEMRStructWidgets.select_encounters(single_selection = False)
518 if not encounters:
519 return u''
520
521 chunks = []
522 for enc in encounters:
523 chunks.append(enc.format_latex (
524 date_format = date_format,
525 soap_cats = cats,
526 soap_order = u'soap_rank, date'
527 ))
528
529 return u''.join(chunks)
530
532
533 cats = list(u'soapu')
534 cats.append(None)
535 template = u'%s'
536 interactive = True
537 line_length = 9999
538 target_format = None
539 time_range = None
540
541 if data is not None:
542 data_parts = data.split('//')
543
544
545 cats = []
546
547 for c in list(data_parts[0]):
548 if c == u' ':
549 c = None
550 cats.append(c)
551
552 if cats == u'':
553 cats = list(u'soapu').append(None)
554
555
556 if len(data_parts) > 1:
557 template = data_parts[1]
558
559
560 if len(data_parts) > 2:
561 try:
562 line_length = int(data_parts[2])
563 except:
564 line_length = 9999
565
566
567 if len(data_parts) > 3:
568 try:
569 time_range = 7 * int(data_parts[3])
570 except:
571 time_range = None
572
573
574 if len(data_parts) > 4:
575 target_format = data_parts[4]
576
577
578 narr = self.pat.get_emr().get_as_journal(soap_cats = cats, time_range = time_range)
579
580 if len(narr) == 0:
581 return u''
582
583 if target_format == u'tex':
584 keys = narr[0].keys()
585 lines = []
586 line_dict = {}
587 for n in narr:
588 for key in keys:
589 if isinstance(n[key], basestring):
590 line_dict[key] = gmTools.tex_escape_string(text = n[key])
591 continue
592 line_dict[key] = n[key]
593 try:
594 lines.append((template % line_dict)[:line_length])
595 except KeyError:
596 return u'invalid key in template [%s], valid keys: %s]' % (template, str(keys))
597 else:
598 try:
599 lines = [ (template % n)[:line_length] for n in narr ]
600 except KeyError:
601 return u'invalid key in template [%s], valid keys: %s]' % (template, str(narr[0].keys()))
602
603 return u'\n'.join(lines)
604
606 return self._get_variant_soap(data=data)
607
609
610
611 cats = list(u'soapu')
612 cats.append(None)
613 template = u'%s'
614
615 if data is not None:
616 data_parts = data.split('//')
617
618
619 cats = []
620
621 for cat in list(data_parts[0]):
622 if cat == u' ':
623 cat = None
624 cats.append(cat)
625
626 if cats == u'':
627 cats = list(u'soapu')
628 cats.append(None)
629
630
631 if len(data_parts) > 1:
632 template = data_parts[1]
633
634
635 narr = gmNarrativeWidgets.select_narrative_from_episodes(soap_cats = cats)
636
637 if narr is None:
638 return u''
639
640 if len(narr) == 0:
641 return u''
642
643 try:
644 narr = [ template % n['narrative'] for n in narr ]
645 except KeyError:
646 return u'invalid key in template [%s], valid keys: %s]' % (template, str(narr[0].keys()))
647
648 return u'\n'.join(narr)
649
668
671
672
674 values = data.split('//', 2)
675
676 if len(values) == 2:
677 male_value, female_value = values
678 other_value = u'<unkown gender>'
679 elif len(values) == 3:
680 male_value, female_value, other_value = values
681 else:
682 return _('invalid gender mapping layout: [%s]') % data
683
684 if self.pat['gender'] == u'm':
685 return male_value
686
687 if self.pat['gender'] == u'f':
688 return female_value
689
690 return other_value
691
692
693
695
696 data_parts = data.split(u'//')
697
698
699 adr_type = data_parts[0].strip()
700 orig_type = adr_type
701 if adr_type != u'':
702 adrs = self.pat.get_addresses(address_type = adr_type)
703 if len(adrs) == 0:
704 _log.warning('no address for type [%s]', adr_type)
705 adr_type = u''
706 if adr_type == u'':
707 _log.debug('asking user for address type')
708 adr = gmPersonContactWidgets.select_address(missing = orig_type, person = self.pat)
709 if adr is None:
710 if self.debug:
711 return _('no address type replacement selected')
712 return u''
713 adr_type = adr['address_type']
714 adr = self.pat.get_addresses(address_type = adr_type)[0]
715
716
717 template = _('%(street)s %(number)s, %(postcode)s %(urb)s, %(l10n_state)s, %(l10n_country)s')
718 if len(data_parts) > 1:
719 if data_parts[1].strip() != u'':
720 template = data_parts[1]
721
722 try:
723 return template % adr.fields_as_dict()
724 except StandardError:
725 _log.exception('error formatting address')
726 _log.error('template: %s', template)
727
728 return None
729
731 requested_type = data.strip()
732 cache_key = 'adr-type-%s' % requested_type
733 try:
734 type2use = self.__cache[cache_key]
735 _log.debug('cache hit (%s): [%s] -> [%s]', cache_key, requested_type, type2use)
736 except KeyError:
737 type2use = requested_type
738 if type2use != u'':
739 adrs = self.pat.get_addresses(address_type = type2use)
740 if len(adrs) == 0:
741 _log.warning('no address of type [%s] for <%s> field extraction', requested_type, part)
742 type2use = u''
743 if type2use == u'':
744 _log.debug('asking user for replacement address type')
745 adr = gmPersonContactWidgets.select_address(missing = requested_type, person = self.pat)
746 if adr is None:
747 _log.debug('no replacement selected')
748 if self.debug:
749 return _('no address type replacement selected')
750 return u''
751 type2use = adr['address_type']
752 self.__cache[cache_key] = type2use
753 _log.debug('caching (%s): [%s] -> [%s]', cache_key, requested_type, type2use)
754
755 return self.pat.get_addresses(address_type = type2use)[0][part]
756
758 return self.__get_variant_adr_part(data = data, part = 'street')
759
761 return self.__get_variant_adr_part(data = data, part = 'number')
762
764 return self.__get_variant_adr_part(data = data, part = 'subunit')
765
767 return self.__get_variant_adr_part(data = data, part = 'urb')
768
770 return self.__get_variant_adr_part(data = data, part = 'suburb')
771
772 - def _get_variant_adr_postcode(self, data=u'?'):
773 return self.__get_variant_adr_part(data = data, part = 'postcode')
774
776 return self.__get_variant_adr_part(data = data, part = 'l10n_state')
777
779 return self.__get_variant_adr_part(data = data, part = 'l10n_country')
780
782 comms = self.pat.get_comm_channels(comm_medium = data)
783 if len(comms) == 0:
784 if self.debug:
785 return _('no URL for comm channel [%s]') % data
786 return u''
787 return comms[0]['url']
788
790
791 template = u'%s'
792 target_mime = None
793 target_ext = None
794 if data is not None:
795 parts = data.split(u'//')
796 template = parts[0]
797 if len(parts) > 1:
798 target_mime = parts[1].strip()
799 if len(parts) > 2:
800 target_ext = parts[2].strip()
801 if target_ext is None:
802 if target_mime is not None:
803 target_ext = gmMimeLib.guess_ext_by_mimetype(mimetype = target_mime)
804
805 mugshot = self.pat.document_folder.latest_mugshot
806 if mugshot is None:
807 if self.debug:
808 return _('no mugshot available')
809 return u''
810
811 fname = mugshot.export_to_file (
812 target_mime = target_mime,
813 target_extension = target_ext,
814 ignore_conversion_problems = True
815 )
816 if fname is None:
817 if self.debug:
818 return _('cannot export or convert latest mugshot')
819 return u''
820
821 return template % fname
822
839
840
841
842
844 data_parts = data.split(u'//')
845 if len(data_parts) < 2:
846 return u'current provider external ID: template is missing'
847
848 id_type = data_parts[0].strip()
849 if id_type == u'':
850 return u'current provider external ID: type is missing'
851
852 issuer = data_parts[1].strip()
853 if issuer == u'':
854 return u'current provider external ID: issuer is missing'
855
856 prov = gmStaff.gmCurrentProvider()
857 ids = prov.identity.get_external_ids(id_type = id_type, issuer = issuer)
858
859 if len(ids) == 0:
860 if self.debug:
861 return _('no external ID [%s] by [%s]') % (id_type, issuer)
862 return u''
863
864 return ids[0]['value']
865
867 data_parts = data.split(u'//')
868 if len(data_parts) < 2:
869 return u'primary in-praxis provider external ID: template is missing'
870
871 id_type = data_parts[0].strip()
872 if id_type == u'':
873 return u'primary in-praxis provider external ID: type is missing'
874
875 issuer = data_parts[1].strip()
876 if issuer == u'':
877 return u'primary in-praxis provider external ID: issuer is missing'
878
879 prov = self.pat.primary_provider
880 if prov is None:
881 if self.debug:
882 return _('no primary in-praxis provider')
883 return u''
884
885 ids = prov.identity.get_external_ids(id_type = id_type, issuer = issuer)
886
887 if len(ids) == 0:
888 if self.debug:
889 return _('no external ID [%s] by [%s]') % (id_type, issuer)
890 return u''
891
892 return ids[0]['value']
893
895 data_parts = data.split(u'//')
896 if len(data_parts) < 2:
897 return u'patient external ID: template is missing'
898
899 id_type = data_parts[0].strip()
900 if id_type == u'':
901 return u'patient external ID: type is missing'
902
903 issuer = data_parts[1].strip()
904 if issuer == u'':
905 return u'patient external ID: issuer is missing'
906
907 ids = self.pat.get_external_ids(id_type = id_type, issuer = issuer)
908
909 if len(ids) == 0:
910 if self.debug:
911 return _('no external ID [%s] by [%s]') % (id_type, issuer)
912 return u''
913
914 return ids[0]['value']
915
917 if data is None:
918 return [_('template is missing')]
919
920 template, separator = data.split('//', 2)
921
922 emr = self.pat.get_emr()
923 return separator.join([ template % a for a in emr.get_allergies() ])
924
932
934
935 if data is None:
936 return [_('template is missing')]
937
938 emr = self.pat.get_emr()
939 current_meds = emr.get_current_substance_intake (
940 include_inactive = False,
941 include_unapproved = False,
942 order_by = u'brand, substance'
943 )
944
945 return u'\n'.join([ data % m.fields_as_dict(date_format = '%Y %B %d') for m in current_meds ])
946
948
949 options = data.split('//')
950
951 if u'latex' in options:
952 return gmMedication.format_substance_intake (
953 emr = self.pat.get_emr(),
954 output_format = u'latex',
955 table_type = u'by-brand'
956 )
957
958 _log.error('no known current medications table formatting style in [%s]', data)
959 return _('unknown current medication table formatting style')
960
962
963 options = data.split('//')
964
965 if u'latex' in options:
966 return gmMedication.format_substance_intake_notes (
967 emr = self.pat.get_emr(),
968 output_format = u'latex',
969 table_type = u'by-brand'
970 )
971
972 _log.error('no known current medications notes formatting style in [%s]', data)
973 return _('unknown current medication notes formatting style')
974
989
1001
1003 options = data.split('//')
1004 template = options[0]
1005 if len(options) > 1:
1006 date_format = options[1]
1007 else:
1008 date_format = u'%Y %B %d'
1009
1010 emr = self.pat.get_emr()
1011 vaccs = emr.get_vaccinations(order_by = u'date_given DESC, vaccine')
1012
1013 return u'\n'.join([ template % v.fields_as_dict(date_format = date_format) for v in vaccs ])
1014
1016
1017 if data is None:
1018 if self.debug:
1019 _log.error('PHX: missing placeholder arguments')
1020 return _('PHX: Invalid placeholder options.')
1021 return u''
1022
1023 _log.debug('arguments: %s', data)
1024
1025 data_parts = data.split(u'//')
1026 template = u'%s'
1027 separator = u'\n'
1028 date_format = '%Y %B %d'
1029 esc_style = None
1030 try:
1031 template = data_parts[0]
1032 separator = data_parts[1]
1033 date_format = data_parts[2]
1034 esc_style = data_parts[3]
1035 except IndexError:
1036 pass
1037
1038 phxs = gmEMRStructWidgets.select_health_issues(emr = self.pat.emr)
1039 if phxs is None:
1040 if self.debug:
1041 return _('no PHX for this patient (available or selected)')
1042 return u''
1043
1044 return separator.join ([
1045 template % phx.fields_as_dict (
1046 date_format = date_format,
1047 escape_style = esc_style,
1048 bool_strings = (_('yes'), _('no'))
1049 ) for phx in phxs
1050 ])
1051
1053
1054 if data is None:
1055 return [_('template is missing')]
1056
1057 probs = self.pat.get_emr().get_problems()
1058
1059 return u'\n'.join([ data % p for p in probs ])
1060
1063
1066
1067 - def _get_variant_text_snippet(self, data=None):
1068 data_parts = data.split(u'//')
1069 keyword = data_parts[0]
1070 template = u'%s'
1071 if len(data_parts) > 1:
1072 template = data_parts[1]
1073
1074 expansion = gmKeywordExpansion.get_expansion (
1075 keyword = keyword,
1076 textual_only = True,
1077 binary_only = False
1078 )
1079 if expansion is None:
1080 if self.debug:
1081 return _('no textual expansion found for keyword <%s>' % keyword)
1082 return u''
1083
1084
1085 return template % expansion['expansion']
1086
1087
1089 parts = data.split(u'//')
1090 keyword = parts[0]
1091 template = u'%s'
1092 target_mime = None
1093 target_ext = None
1094 if len(parts) > 1:
1095 template = parts[1]
1096 if len(parts) > 2:
1097 target_mime = parts[2].strip()
1098 if len(parts) > 3:
1099 target_ext = parts[3].strip()
1100 if target_ext is None:
1101 if target_mime is not None:
1102 target_ext = gmMimeLib.guess_ext_by_mimetype(mimetype = target_mime)
1103
1104 expansion = gmKeywordExpansion.get_expansion (
1105 keyword = keyword,
1106 textual_only = False,
1107 binary_only = True
1108 )
1109 if expansion is None:
1110 if self.debug:
1111 return _('no binary expansion found for keyword <%s>' % keyword)
1112 return u''
1113
1114 filename = expansion.export_to_file (
1115 target_mime = target_mime,
1116 target_extension = target_ext,
1117 ignore_conversion_problems = True
1118 )
1119 if filename is None:
1120 if self.debug:
1121 return _('cannot export or convert data for binary expansion keyword <%s>' % keyword)
1122 return u''
1123
1124
1125 return template % filename
1126
1127
1128 - def _get_variant_free_text(self, data=u'tex//'):
1129
1130
1131
1132
1133 data_parts = data.split('//')
1134 format = data_parts[0]
1135 if len(data_parts) > 1:
1136 msg = data_parts[1]
1137 else:
1138 msg = _('generic text')
1139
1140 dlg = gmGuiHelpers.cMultilineTextEntryDlg (
1141 None,
1142 -1,
1143 title = _('Replacing <free_text> placeholder'),
1144 msg = _('Below you can enter free text.\n\n [%s]') % msg
1145 )
1146 dlg.enable_user_formatting = True
1147 decision = dlg.ShowModal()
1148
1149 if decision != wx.ID_SAVE:
1150 dlg.Destroy()
1151 if self.debug:
1152 return _('Text input cancelled by user.')
1153 return u''
1154
1155 text = dlg.value.strip()
1156 if dlg.is_user_formatted:
1157 dlg.Destroy()
1158 return text
1159
1160 dlg.Destroy()
1161
1162 if format == u'tex':
1163 return gmTools.tex_escape_string(text = text)
1164
1165 return text
1166
1180
1194
1195
1196
1199
1201 """Functions a macro can legally use.
1202
1203 An instance of this class is passed to the GNUmed scripting
1204 listener. Hence, all actions a macro can legally take must
1205 be defined in this class. Thus we achieve some screening for
1206 security and also thread safety handling.
1207 """
1208
1209 - def __init__(self, personality = None):
1210 if personality is None:
1211 raise gmExceptions.ConstructorError, 'must specify personality'
1212 self.__personality = personality
1213 self.__attached = 0
1214 self._get_source_personality = None
1215 self.__user_done = False
1216 self.__user_answer = 'no answer yet'
1217 self.__pat = gmPerson.gmCurrentPatient()
1218
1219 self.__auth_cookie = str(random.random())
1220 self.__pat_lock_cookie = str(random.random())
1221 self.__lock_after_load_cookie = str(random.random())
1222
1223 _log.info('slave mode personality is [%s]', personality)
1224
1225
1226
1227 - def attach(self, personality = None):
1228 if self.__attached:
1229 _log.error('attach with [%s] rejected, already serving a client', personality)
1230 return (0, _('attach rejected, already serving a client'))
1231 if personality != self.__personality:
1232 _log.error('rejecting attach to personality [%s], only servicing [%s]' % (personality, self.__personality))
1233 return (0, _('attach to personality [%s] rejected') % personality)
1234 self.__attached = 1
1235 self.__auth_cookie = str(random.random())
1236 return (1, self.__auth_cookie)
1237
1238 - def detach(self, auth_cookie=None):
1239 if not self.__attached:
1240 return 1
1241 if auth_cookie != self.__auth_cookie:
1242 _log.error('rejecting detach() with cookie [%s]' % auth_cookie)
1243 return 0
1244 self.__attached = 0
1245 return 1
1246
1248 if not self.__attached:
1249 return 1
1250 self.__user_done = False
1251
1252 wx.CallAfter(self._force_detach)
1253 return 1
1254
1256 ver = _cfg.get(option = u'client_version')
1257 return "GNUmed %s, %s $Revision: 1.51 $" % (ver, self.__class__.__name__)
1258
1260 """Shuts down this client instance."""
1261 if not self.__attached:
1262 return 0
1263 if auth_cookie != self.__auth_cookie:
1264 _log.error('non-authenticated shutdown_gnumed()')
1265 return 0
1266 wx.CallAfter(self._shutdown_gnumed, forced)
1267 return 1
1268
1270 """Raise ourselves to the top of the desktop."""
1271 if not self.__attached:
1272 return 0
1273 if auth_cookie != self.__auth_cookie:
1274 _log.error('non-authenticated raise_gnumed()')
1275 return 0
1276 return "cMacroPrimitives.raise_gnumed() not implemented"
1277
1279 if not self.__attached:
1280 return 0
1281 if auth_cookie != self.__auth_cookie:
1282 _log.error('non-authenticated get_loaded_plugins()')
1283 return 0
1284 gb = gmGuiBroker.GuiBroker()
1285 return gb['horstspace.notebook.gui'].keys()
1286
1288 """Raise a notebook plugin within GNUmed."""
1289 if not self.__attached:
1290 return 0
1291 if auth_cookie != self.__auth_cookie:
1292 _log.error('non-authenticated raise_notebook_plugin()')
1293 return 0
1294
1295 wx.CallAfter(gmPlugin.raise_notebook_plugin, a_plugin)
1296 return 1
1297
1299 """Load external patient, perhaps create it.
1300
1301 Callers must use get_user_answer() to get status information.
1302 It is unsafe to proceed without knowing the completion state as
1303 the controlled client may be waiting for user input from a
1304 patient selection list.
1305 """
1306 if not self.__attached:
1307 return (0, _('request rejected, you are not attach()ed'))
1308 if auth_cookie != self.__auth_cookie:
1309 _log.error('non-authenticated load_patient_from_external_source()')
1310 return (0, _('rejected load_patient_from_external_source(), not authenticated'))
1311 if self.__pat.locked:
1312 _log.error('patient is locked, cannot load from external source')
1313 return (0, _('current patient is locked'))
1314 self.__user_done = False
1315 wx.CallAfter(self._load_patient_from_external_source)
1316 self.__lock_after_load_cookie = str(random.random())
1317 return (1, self.__lock_after_load_cookie)
1318
1320 if not self.__attached:
1321 return (0, _('request rejected, you are not attach()ed'))
1322 if auth_cookie != self.__auth_cookie:
1323 _log.error('non-authenticated lock_load_patient()')
1324 return (0, _('rejected lock_load_patient(), not authenticated'))
1325
1326 if lock_after_load_cookie != self.__lock_after_load_cookie:
1327 _log.warning('patient lock-after-load request rejected due to wrong cookie [%s]' % lock_after_load_cookie)
1328 return (0, 'patient lock-after-load request rejected, wrong cookie provided')
1329 self.__pat.locked = True
1330 self.__pat_lock_cookie = str(random.random())
1331 return (1, self.__pat_lock_cookie)
1332
1334 if not self.__attached:
1335 return (0, _('request rejected, you are not attach()ed'))
1336 if auth_cookie != self.__auth_cookie:
1337 _log.error('non-authenticated lock_into_patient()')
1338 return (0, _('rejected lock_into_patient(), not authenticated'))
1339 if self.__pat.locked:
1340 _log.error('patient is already locked')
1341 return (0, _('already locked into a patient'))
1342 searcher = gmPersonSearch.cPatientSearcher_SQL()
1343 if type(search_params) == types.DictType:
1344 idents = searcher.get_identities(search_dict=search_params)
1345 raise StandardError("must use dto, not search_dict")
1346 else:
1347 idents = searcher.get_identities(search_term=search_params)
1348 if idents is None:
1349 return (0, _('error searching for patient with [%s]/%s') % (search_term, search_dict))
1350 if len(idents) == 0:
1351 return (0, _('no patient found for [%s]/%s') % (search_term, search_dict))
1352
1353 if len(idents) > 1:
1354 return (0, _('several matching patients found for [%s]/%s') % (search_term, search_dict))
1355 if not gmPatSearchWidgets.set_active_patient(patient = idents[0]):
1356 return (0, _('cannot activate patient [%s] (%s/%s)') % (str(idents[0]), search_term, search_dict))
1357 self.__pat.locked = True
1358 self.__pat_lock_cookie = str(random.random())
1359 return (1, self.__pat_lock_cookie)
1360
1362 if not self.__attached:
1363 return (0, _('request rejected, you are not attach()ed'))
1364 if auth_cookie != self.__auth_cookie:
1365 _log.error('non-authenticated unlock_patient()')
1366 return (0, _('rejected unlock_patient, not authenticated'))
1367
1368 if not self.__pat.locked:
1369 return (1, '')
1370
1371 if unlock_cookie != self.__pat_lock_cookie:
1372 _log.warning('patient unlock request rejected due to wrong cookie [%s]' % unlock_cookie)
1373 return (0, 'patient unlock request rejected, wrong cookie provided')
1374 self.__pat.locked = False
1375 return (1, '')
1376
1378 if not self.__attached:
1379 return 0
1380 if auth_cookie != self.__auth_cookie:
1381 _log.error('non-authenticated select_identity()')
1382 return 0
1383 return "cMacroPrimitives.assume_staff_identity() not implemented"
1384
1386 if not self.__user_done:
1387 return (0, 'still waiting')
1388 self.__user_done = False
1389 return (1, self.__user_answer)
1390
1391
1392
1394 msg = _(
1395 'Someone tries to forcibly break the existing\n'
1396 'controlling connection. This may or may not\n'
1397 'have legitimate reasons.\n\n'
1398 'Do you want to allow breaking the connection ?'
1399 )
1400 can_break_conn = gmGuiHelpers.gm_show_question (
1401 aMessage = msg,
1402 aTitle = _('forced detach attempt')
1403 )
1404 if can_break_conn:
1405 self.__user_answer = 1
1406 else:
1407 self.__user_answer = 0
1408 self.__user_done = True
1409 if can_break_conn:
1410 self.__pat.locked = False
1411 self.__attached = 0
1412 return 1
1413
1415 top_win = wx.GetApp().GetTopWindow()
1416 if forced:
1417 top_win.Destroy()
1418 else:
1419 top_win.Close()
1420
1429
1430
1431
1432 if __name__ == '__main__':
1433
1434 if len(sys.argv) < 2:
1435 sys.exit()
1436
1437 if sys.argv[1] != 'test':
1438 sys.exit()
1439
1440 gmI18N.activate_locale()
1441 gmI18N.install_domain()
1442
1443
1445 handler = gmPlaceholderHandler()
1446 handler.debug = True
1447
1448 for placeholder in ['a', 'b']:
1449 print handler[placeholder]
1450
1451 pat = gmPersonSearch.ask_for_patient()
1452 if pat is None:
1453 return
1454
1455 gmPatSearchWidgets.set_active_patient(patient = pat)
1456
1457 print 'DOB (YYYY-MM-DD):', handler['date_of_birth::%Y-%m-%d']
1458
1459 app = wx.PyWidgetTester(size = (200, 50))
1460 for placeholder in known_placeholders:
1461 print placeholder, "=", handler[placeholder]
1462
1463 ph = 'progress_notes::ap'
1464 print '%s: %s' % (ph, handler[ph])
1465
1467
1468 tests = [
1469
1470 '$<lastname>$',
1471 '$<lastname::::3>$',
1472 '$<name::%(title)s %(firstnames)s%(preferred)s%(lastnames)s>$',
1473
1474
1475 'lastname',
1476 '$<lastname',
1477 '$<lastname::',
1478 '$<lastname::>$',
1479 '$<lastname::abc>$',
1480 '$<lastname::abc::>$',
1481 '$<lastname::abc::3>$',
1482 '$<lastname::abc::xyz>$',
1483 '$<lastname::::>$',
1484 '$<lastname::::xyz>$',
1485
1486 '$<date_of_birth::%Y-%m-%d>$',
1487 '$<date_of_birth::%Y-%m-%d::3>$',
1488 '$<date_of_birth::%Y-%m-%d::>$',
1489
1490
1491 '$<adr_location::home::35>$',
1492 '$<gender_mapper::male//female//other::5>$',
1493 '$<current_meds::==> %(brand)s %(preparation)s (%(substance)s) <==\n::50>$',
1494 '$<allergy_list::%(descriptor)s, >$',
1495 '$<current_meds_table::latex//by-brand>$'
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510 ]
1511
1512
1513
1514
1515
1516 pat = gmPersonSearch.ask_for_patient()
1517 if pat is None:
1518 return
1519
1520 gmPatSearchWidgets.set_active_patient(patient = pat)
1521
1522 handler = gmPlaceholderHandler()
1523 handler.debug = True
1524
1525 for placeholder in tests:
1526 print placeholder, "=>", handler[placeholder]
1527 print "--------------"
1528 raw_input()
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1541 from Gnumed.pycommon import gmScriptingListener
1542 import xmlrpclib
1543 listener = gmScriptingListener.cScriptingListener(macro_executor = cMacroPrimitives(personality='unit test'), port=9999)
1544
1545 s = xmlrpclib.ServerProxy('http://localhost:9999')
1546 print "should fail:", s.attach()
1547 print "should fail:", s.attach('wrong cookie')
1548 print "should work:", s.version()
1549 print "should fail:", s.raise_gnumed()
1550 print "should fail:", s.raise_notebook_plugin('test plugin')
1551 print "should fail:", s.lock_into_patient('kirk, james')
1552 print "should fail:", s.unlock_patient()
1553 status, conn_auth = s.attach('unit test')
1554 print "should work:", status, conn_auth
1555 print "should work:", s.version()
1556 print "should work:", s.raise_gnumed(conn_auth)
1557 status, pat_auth = s.lock_into_patient(conn_auth, 'kirk, james')
1558 print "should work:", status, pat_auth
1559 print "should fail:", s.unlock_patient(conn_auth, 'bogus patient unlock cookie')
1560 print "should work", s.unlock_patient(conn_auth, pat_auth)
1561 data = {'firstname': 'jame', 'lastnames': 'Kirk', 'gender': 'm'}
1562 status, pat_auth = s.lock_into_patient(conn_auth, data)
1563 print "should work:", status, pat_auth
1564 print "should work", s.unlock_patient(conn_auth, pat_auth)
1565 print s.detach('bogus detach cookie')
1566 print s.detach(conn_auth)
1567 del s
1568
1569 listener.shutdown()
1570
1572
1573 import re as regex
1574
1575 tests = [
1576 ' $<lastname>$ ',
1577 ' $<lastname::::3>$ ',
1578
1579
1580 '$<date_of_birth::%Y-%m-%d>$',
1581 '$<date_of_birth::%Y-%m-%d::3>$',
1582 '$<date_of_birth::%Y-%m-%d::>$',
1583
1584 '$<adr_location::home::35>$',
1585 '$<gender_mapper::male//female//other::5>$',
1586 '$<current_meds::==> %(brand)s %(preparation)s (%(substance)s) <==\\n::50>$',
1587 '$<allergy_list::%(descriptor)s, >$',
1588
1589 '\\noindent Patient: $<lastname>$, $<firstname>$',
1590 '$<allergies::%(descriptor)s & %(l10n_type)s & {\\footnotesize %(reaction)s} \tabularnewline \hline >$',
1591 '$<current_meds:: \item[%(substance)s] {\\footnotesize (%(brand)s)} %(preparation)s %(amount)s%(unit)s: %(schedule)s >$'
1592 ]
1593
1594 tests = [
1595
1596 'junk $<lastname::::3>$ junk',
1597 'junk $<lastname::abc::3>$ junk',
1598 'junk $<lastname::abc>$ junk',
1599 'junk $<lastname>$ junk',
1600
1601 'junk $<lastname>$ junk $<firstname>$ junk',
1602 'junk $<lastname::abc>$ junk $<fiststname::abc>$ junk',
1603 'junk $<lastname::abc::3>$ junk $<firstname::abc::3>$ junk',
1604 'junk $<lastname::::3>$ junk $<firstname::::3>$ junk'
1605
1606 ]
1607
1608 print "testing placeholder regex:", default_placeholder_regex
1609 print ""
1610
1611 for t in tests:
1612 print 'line: "%s"' % t
1613 print "placeholders:"
1614 for p in regex.findall(default_placeholder_regex, t, regex.IGNORECASE):
1615 print ' => "%s"' % p
1616 print " "
1617
1619
1620 phs = [
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648 u'$<data_snippet::binary_test_snippet//path=<%s>//image/png//.png::250>$',
1649 u'$<data_snippet::autograph-LMcC//path=<%s>//image/jpg//.jpg::250>$'
1650 ]
1651
1652 handler = gmPlaceholderHandler()
1653 handler.debug = True
1654
1655 gmStaff.set_current_provider_to_logged_on_user()
1656 pat = gmPersonSearch.ask_for_patient()
1657 if pat is None:
1658 return
1659
1660 gmPatSearchWidgets.set_active_patient(patient = pat)
1661
1662 app = wx.PyWidgetTester(size = (200, 50))
1663
1664 for ph in phs:
1665 print ph
1666 print "result:"
1667 print '%s' % handler[ph]
1668
1669
1670
1671
1672
1673
1674
1675 test_placeholder()
1676
1677
1678