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 known_placeholders = [
62 u'lastname',
63 u'firstname',
64 u'title',
65 u'date_of_birth',
66 u'progress_notes',
67 u'soap',
68 u'soap_s',
69 u'soap_o',
70 u'soap_a',
71 u'soap_p',
72 u'soap_u',
73 u'client_version',
74 u'current_provider',
75 u'primary_praxis_provider',
76 u'allergy_state'
77 ]
78
79
80
81
82
83 _injectable_placeholders = {
84 u'form_name_long': None,
85 u'form_name_short': None,
86 u'form_version': None
87 }
88
89
90
91 known_variant_placeholders = [
92
93 u'free_text',
94 u'text_snippet',
95
96 u'data_snippet',
97
98
99
100
101
102
103 u'tex_escape',
104 u'today',
105 u'gender_mapper',
106
107
108
109
110 u'name',
111 u'date_of_birth',
112
113 u'patient_address',
114 u'adr_street',
115 u'adr_number',
116 u'adr_subunit',
117 u'adr_location',
118 u'adr_suburb',
119 u'adr_postcode',
120 u'adr_region',
121 u'adr_country',
122
123 u'patient_comm',
124 u'patient_tags',
125
126
127 u'patient_photo',
128
129
130
131
132
133
134
135 u'external_id',
136
137
138
139 u'soap',
140 u'progress_notes',
141
142
143
144 u'soap_for_encounters',
145
146
147
148 u'soap_by_issue',
149
150
151
152 u'soap_by_episode',
153
154
155
156 u'emr_journal',
157
158
159
160
161
162
163
164
165
166 u'current_meds',
167
168 u'current_meds_table',
169 u'current_meds_notes',
170
171 u'lab_table',
172 u'test_results',
173
174 u'latest_vaccs_table',
175 u'vaccination_history',
176
177 u'allergies',
178 u'allergy_list',
179 u'problems',
180 u'PHX',
181 u'encounter_list',
182
183 u'documents',
184
185
186
187
188
189
190
191
192
193
194
195
196
197 u'reminders',
198
199
200
201
202 u'current_provider_external_id',
203 u'primary_praxis_provider_external_id',
204
205
206 u'bill',
207 u'bill_item'
208 ]
209
210
211 default_placeholder_regex = r'\$<.+?>\$'
212
213
214 default_placeholder_start = u'$<'
215 default_placeholder_end = u'>$'
216
218 fname = gmTools.get_unique_filename(prefix = 'gm-placeholders-', suffix = '.txt')
219 ph_file = codecs.open(filename = fname, mode = 'wb', encoding = 'utf8', errors = 'replace')
220
221 ph_file.write(u'Here you can find some more documentation on placeholder use:\n')
222 ph_file.write(u'\n http://wiki.gnumed.de/bin/view/Gnumed/GmManualLettersForms\n\n\n')
223
224 ph_file.write(u'Simple placeholders (use like: $<PLACEHOLDER_NAME>$):\n')
225 for ph in known_placeholders:
226 ph_file.write(u' %s\n' % ph)
227 ph_file.write(u'\n')
228
229 ph_file.write(u'Variable placeholders (use like: $<PLACEHOLDER_NAME::ARGUMENTS::MAX OUTPUT LENGTH>$):\n')
230 for ph in known_variant_placeholders:
231 ph_file.write(u' %s\n' % ph)
232 ph_file.write(u'\n')
233
234 ph_file.write(u'Injectable placeholders (use like: $<PLACEHOLDER_NAME::ARGUMENTS::MAX OUTPUT LENGTH>$):\n')
235 for ph in _injectable_placeholders:
236 ph_file.write(u' %s\n' % ph)
237 ph_file.write(u'\n')
238
239 ph_file.close()
240 gmMimeLib.call_viewer_on_file(aFile = fname, block = False)
241
243 """Returns values for placeholders.
244
245 - patient related placeholders operate on the currently active patient
246 - is passed to the forms handling code, for example
247
248 Return values when .debug is False:
249 - errors with placeholders return None
250 - placeholders failing to resolve to a value return an empty string
251
252 Return values when .debug is True:
253 - errors with placeholders return an error string
254 - placeholders failing to resolve to a value return a warning string
255
256 There are several types of placeholders:
257
258 simple static placeholders
259 - those are listed in known_placeholders
260 - they are used as-is
261
262 extended static placeholders
263 - those are, effectively, static placeholders
264 with a maximum length attached (after "::::")
265
266 injectable placeholders
267 - they must be set up before use by set_placeholder()
268 - they should be removed after use by unset_placeholder()
269 - the syntax is like extended static placeholders
270 - they are listed in _injectable_placeholders
271
272 variant placeholders
273 - those are listed in known_variant_placeholders
274 - they are parsed into placeholder, data, and maximum length
275 - the length is optional
276 - data is passed to the handler
277
278 Note that this cannot be called from a non-gui thread unless
279 wrapped in wx.CallAfter().
280 """
282
283 self.pat = gmPerson.gmCurrentPatient()
284 self.debug = False
285
286 self.invalid_placeholder_template = _('invalid placeholder [%s]')
287
288 self.__cache = {}
289
290 self.__esc_style = None
291 self.__esc_func = lambda x:x
292
293
294
298
302
304 self.__cache[key] = value
305
307 del self.__cache[key]
308
312
313 escape_style = property(lambda x:x, _set_escape_style)
314
323
324 escape_function = property(lambda x:x, _set_escape_function)
325
326
327
329 """Map self['placeholder'] to self.placeholder.
330
331 This is useful for replacing placeholders parsed out
332 of documents as strings.
333
334 Unknown/invalid placeholders still deliver a result but
335 it will be glaringly obvious if debugging is enabled.
336 """
337 _log.debug('replacing [%s]', placeholder)
338
339 original_placeholder = placeholder
340
341 if placeholder.startswith(default_placeholder_start):
342 placeholder = placeholder[len(default_placeholder_start):]
343 if placeholder.endswith(default_placeholder_end):
344 placeholder = placeholder[:-len(default_placeholder_end)]
345 else:
346 _log.debug('placeholder must either start with [%s] and end with [%s] or neither of both', default_placeholder_start, default_placeholder_end)
347 if self.debug:
348 return self.invalid_placeholder_template % original_placeholder
349 return None
350
351
352 if placeholder in known_placeholders:
353 return getattr(self, placeholder)
354
355
356 parts = placeholder.split('::::', 1)
357 if len(parts) == 2:
358 name, lng = parts
359 unknown_injectable = False
360 try:
361 val = _injectable_placeholders[name]
362 except KeyError:
363 unknown_injectable = True
364 except:
365 _log.exception('placeholder handling error: %s', original_placeholder)
366 if self.debug:
367 return self.invalid_placeholder_template % original_placeholder
368 return None
369 if not unknown_injectable:
370 if val is None:
371 if self.debug:
372 return u'injectable placeholder [%s]: no value available' % name
373 return placeholder
374 return val[:int(lng)]
375
376
377 parts = placeholder.split('::::', 1)
378 if len(parts) == 2:
379 name, lng = parts
380 try:
381 return getattr(self, name)[:int(lng)]
382 except:
383 _log.exception('placeholder handling error: %s', original_placeholder)
384 if self.debug:
385 return self.invalid_placeholder_template % original_placeholder
386 return None
387
388
389 parts = placeholder.split('::')
390
391 if len(parts) == 1:
392 _log.warning('invalid placeholder layout: %s', original_placeholder)
393 if self.debug:
394 return self.invalid_placeholder_template % original_placeholder
395 return None
396
397 if len(parts) == 2:
398 name, data = parts
399 lng = None
400
401 if len(parts) == 3:
402 name, data, lng = parts
403 try:
404 lng = int(lng)
405 except (TypeError, ValueError):
406 _log.debug('placeholder length definition error: %s, discarding length: >%s<', original_placeholder, lng)
407 lng = None
408
409 if len(parts) > 3:
410 _log.warning('invalid placeholder layout: %s', original_placeholder)
411 if self.debug:
412 return self.invalid_placeholder_template % original_placeholder
413 return None
414
415 handler = getattr(self, '_get_variant_%s' % name, None)
416 if handler is None:
417 _log.warning('no handler <_get_variant_%s> for placeholder %s', name, original_placeholder)
418 if self.debug:
419 return self.invalid_placeholder_template % original_placeholder
420 return None
421
422 try:
423 if lng is None:
424 return handler(data = data)
425 return handler(data = data)[:lng]
426 except:
427 _log.exception('placeholder handling error: %s', original_placeholder)
428 if self.debug:
429 return self.invalid_placeholder_template % original_placeholder
430 return None
431
432 _log.error('something went wrong, should never get here')
433 return None
434
435
436
437
438
440 """This does nothing, used as a NOOP properties setter."""
441 pass
442
445
448
451
453 return self._get_variant_date_of_birth(data = '%Y %b %d')
454
456 return self._get_variant_soap()
457
459 return self._get_variant_soap(data = u's')
460
462 return self._get_variant_soap(data = u'o')
463
465 return self._get_variant_soap(data = u'a')
466
468 return self._get_variant_soap(data = u'p')
469
471 return self._get_variant_soap(data = u'u')
472
474 return self._get_variant_soap(soap_cats = None)
475
477 return self._escape (
478 gmTools.coalesce (
479 _cfg.get(option = u'client_version'),
480 u'%s' % self.__class__.__name__
481 )
482 )
483
500
515
517 allg_state = self.pat.get_emr().allergy_state
518
519 if allg_state['last_confirmed'] is None:
520 date_confirmed = u''
521 else:
522 date_confirmed = u' (%s)' % gmDateTime.pydt_strftime (
523 allg_state['last_confirmed'],
524 format = '%Y %B %d'
525 )
526
527 tmp = u'%s%s' % (
528 allg_state.state_string,
529 date_confirmed
530 )
531 return self._escape(tmp)
532
533
534
535 placeholder_regex = property(lambda x: default_placeholder_regex, _setter_noop)
536
537
538
539
540 lastname = property(_get_lastname, _setter_noop)
541 firstname = property(_get_firstname, _setter_noop)
542 title = property(_get_title, _setter_noop)
543 date_of_birth = property(_get_dob, _setter_noop)
544
545 progress_notes = property(_get_progress_notes, _setter_noop)
546 soap = property(_get_progress_notes, _setter_noop)
547 soap_s = property(_get_soap_s, _setter_noop)
548 soap_o = property(_get_soap_o, _setter_noop)
549 soap_a = property(_get_soap_a, _setter_noop)
550 soap_p = property(_get_soap_p, _setter_noop)
551 soap_u = property(_get_soap_u, _setter_noop)
552 soap_admin = property(_get_soap_admin, _setter_noop)
553
554 allergy_state = property(_get_allergy_state, _setter_noop)
555
556 client_version = property(_get_client_version, _setter_noop)
557
558 current_provider = property(_get_current_provider, _setter_noop)
559 primary_praxis_provider = property(_get_primary_praxis_provider, _setter_noop)
560
561
562
564
565 from Gnumed.wxpython import gmProviderInboxWidgets
566
567 template = _('due %(due_date)s: %(comment)s (%(interval_due)s)')
568 date_format = '%Y %b %d'
569
570 data_parts = data.split('//')
571
572 if len(data_parts) > 0:
573 if data_parts[0].strip() != u'':
574 template = data_parts[0]
575
576 if len(data_parts) > 1:
577 if data_parts[1].strip() != u'':
578 date_format = data_parts[1]
579
580 reminders = gmProviderInboxWidgets.manage_reminders(patient = self.pat.ID)
581
582 if reminders is None:
583 return u''
584
585 if len(reminders) == 0:
586 return u''
587
588 lines = [ template % r.fields_as_dict(date_format = date_format, escape_style = self.__esc_style) for r in reminders ]
589
590 return u'\n'.join(lines)
591
593
594 select = False
595 include_descriptions = False
596 template = u'%s'
597 path_template = None
598 export_path = None
599
600 data_parts = data.split('//')
601
602 if u'select' in data_parts:
603 select = True
604 data_parts.remove(u'select')
605
606 if u'description' in data_parts:
607 include_descriptions = True
608 data_parts.remove(u'description')
609
610 template = data_parts[0]
611
612 if len(data_parts) > 1:
613 path_template = data_parts[1]
614
615 if len(data_parts) > 2:
616 export_path = data_parts[2]
617
618
619 if export_path is not None:
620 export_path = os.path.normcase(os.path.expanduser(export_path))
621 gmTools.mkdir(export_path)
622
623
624 if select:
625 docs = gmDocumentWidgets.manage_documents(msg = _('Select the patient documents to reference from the new document.'), single_selection = False)
626 else:
627 docs = self.pat.document_folder.documents
628
629 if docs is None:
630 return u''
631
632 lines = []
633 for doc in docs:
634 lines.append(template % doc.fields_as_dict(date_format = '%Y %b %d', escape_style = self.__esc_style))
635 if include_descriptions:
636 for desc in doc.get_descriptions(max_lng = None):
637 lines.append(self._escape(desc['text'] + u'\n'))
638 if path_template is not None:
639 for part_name in doc.export_parts_to_files(export_dir = export_path):
640 path, name = os.path.split(part_name)
641 lines.append(path_template % {'fullpath': part_name, 'name': name})
642
643 return u'\n'.join(lines)
644
646
647 encounters = gmEMRStructWidgets.select_encounters(single_selection = False)
648 if not encounters:
649 return u''
650
651 template = data
652
653 lines = []
654 for enc in encounters:
655 try:
656 lines.append(template % enc.fields_as_dict(date_format = '%Y %b %d', escape_style = self.__esc_style))
657 except:
658 lines.append(u'error formatting encounter')
659 _log.exception('problem formatting encounter list')
660 _log.error('template: %s', template)
661 _log.error('encounter: %s', encounter)
662
663 return u'\n'.join(lines)
664
666 """Select encounters from list and format SOAP thereof.
667
668 data: soap_cats (' ' -> None -> admin) // date format
669 """
670
671 cats = None
672 date_format = None
673
674 if data is not None:
675 data_parts = data.split('//')
676
677
678 if len(data_parts[0]) > 0:
679 cats = []
680 if u' ' in data_parts[0]:
681 cats.append(None)
682 data_parts[0] = data_parts[0].replace(u' ', u'')
683 cats.extend(list(data_parts[0]))
684
685
686 if len(data_parts) > 1:
687 if len(data_parts[1]) > 0:
688 date_format = data_parts[1]
689
690 encounters = gmEMRStructWidgets.select_encounters(single_selection = False)
691 if not encounters:
692 return u''
693
694 chunks = []
695 for enc in encounters:
696 chunks.append(enc.format_latex (
697 date_format = date_format,
698 soap_cats = cats,
699 soap_order = u'soap_rank, date'
700 ))
701
702 return u''.join(chunks)
703
705
706 cats = list(u'soapu')
707 cats.append(None)
708 template = u'%s'
709 interactive = True
710 line_length = 9999
711 target_format = None
712 time_range = None
713
714 if data is not None:
715 data_parts = data.split('//')
716
717
718 cats = []
719
720 for c in list(data_parts[0]):
721 if c == u' ':
722 c = None
723 cats.append(c)
724
725 if cats == u'':
726 cats = list(u'soapu').append(None)
727
728
729 if len(data_parts) > 1:
730 template = data_parts[1]
731
732
733 if len(data_parts) > 2:
734 try:
735 line_length = int(data_parts[2])
736 except:
737 line_length = 9999
738
739
740 if len(data_parts) > 3:
741 try:
742 time_range = 7 * int(data_parts[3])
743 except:
744
745
746 time_range = data_parts[3]
747
748
749 if len(data_parts) > 4:
750 target_format = data_parts[4]
751
752
753 narr = self.pat.emr.get_as_journal(soap_cats = cats, time_range = time_range)
754
755 if len(narr) == 0:
756 return u''
757
758 keys = narr[0].keys()
759 lines = []
760 line_dict = {}
761 for n in narr:
762 for key in keys:
763 if isinstance(n[key], basestring):
764 line_dict[key] = self._escape(text = n[key])
765 continue
766 line_dict[key] = n[key]
767 try:
768 lines.append((template % line_dict)[:line_length])
769 except KeyError:
770 return u'invalid key in template [%s], valid keys: %s]' % (template, str(keys))
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794 return u'\n'.join(lines)
795
797 return self.__get_variant_soap_by_issue_or_episode(data = data, mode = u'issue')
798
800 return self.__get_variant_soap_by_issue_or_episode(data = data, mode = u'episode')
801
803
804
805 cats = list(u'soapu')
806 cats.append(None)
807
808 date_format = None
809 template = u'%s'
810
811 if data is not None:
812 data_parts = data.split('//')
813
814
815 if len(data_parts[0]) > 0:
816 cats = []
817 if u' ' in data_parts[0]:
818 cats.append(None)
819 cats.extend(list(data_parts[0].replace(u' ', u'')))
820
821
822 if len(data_parts) > 1:
823 if len(data_parts[1]) > 0:
824 date_format = data_parts[1]
825
826
827 if len(data_parts) > 2:
828 if len(data_parts[2]) > 0:
829 template = data_parts[2]
830
831 if mode == u'issue':
832 narr = gmNarrativeWidgets.select_narrative_by_issue(soap_cats = cats)
833 else:
834 narr = gmNarrativeWidgets.select_narrative_by_episode(soap_cats = cats)
835
836 if narr is None:
837 return u''
838
839 if len(narr) == 0:
840 return u''
841
842 try:
843 narr = [ template % n.fields_as_dict(date_format = date_format, escape_style = self.__esc_style) for n in narr ]
844 except KeyError:
845 return u'invalid key in template [%s], valid keys: %s]' % (template, str(narr[0].keys()))
846
847 return u'\n'.join(narr)
848
850 return self._get_variant_soap(data=data)
851
853
854
855 cats = list(u'soapu')
856 cats.append(None)
857 template = u'%(narrative)s'
858
859 if data is not None:
860 data_parts = data.split('//')
861
862
863 cats = []
864
865 for cat in list(data_parts[0]):
866 if cat == u' ':
867 cat = None
868 cats.append(cat)
869
870 if cats == u'':
871 cats = list(u'soapu')
872 cats.append(None)
873
874
875 if len(data_parts) > 1:
876 template = data_parts[1]
877
878 narr = gmNarrativeWidgets.select_narrative_from_episodes(soap_cats = cats)
879
880 if narr is None:
881 return u''
882
883 if len(narr) == 0:
884 return u''
885
886
887
888
889 if u'%s' in template:
890 narr = [ self._escape(n['narrative']) for n in narr ]
891 else:
892 narr = [ n.fields_as_dict(escape_style = self.__esc_style) for n in narr ]
893
894 try:
895 narr = [ template % n for n in narr ]
896 except KeyError:
897 return u'invalid key in template [%s], valid keys: %s]' % (template, str(narr[0].keys()))
898 except TypeError:
899 return u'cannot mix "%%s" and "%%(key)s" in template [%s]' % template
900
901 return u'\n'.join(narr)
902
904 if data is None:
905 return [_('template is missing')]
906
907 name = self.pat.get_active_name()
908
909 parts = {
910 'title': self._escape(gmTools.coalesce(name['title'], u'')),
911 'firstnames': self._escape(name['firstnames']),
912 'lastnames': self._escape(name['lastnames']),
913 'preferred': self._escape(gmTools.coalesce (
914 initial = name['preferred'],
915 instead = u' ',
916 template_initial = u' "%s" '
917 ))
918 }
919
920 return data % parts
921
924
925
927
928 values = data.split('//', 2)
929
930 if len(values) == 2:
931 male_value, female_value = values
932 other_value = u'<unkown gender>'
933 elif len(values) == 3:
934 male_value, female_value, other_value = values
935 else:
936 return _('invalid gender mapping layout: [%s]') % data
937
938 if self.pat['gender'] == u'm':
939 return self._escape(male_value)
940
941 if self.pat['gender'] == u'f':
942 return self._escape(female_value)
943
944 return self._escape(other_value)
945
946
947
949
950 data_parts = data.split(u'//')
951
952
953 adr_type = data_parts[0].strip()
954 orig_type = adr_type
955 if adr_type != u'':
956 adrs = self.pat.get_addresses(address_type = adr_type)
957 if len(adrs) == 0:
958 _log.warning('no address for type [%s]', adr_type)
959 adr_type = u''
960 if adr_type == u'':
961 _log.debug('asking user for address type')
962 adr = gmPersonContactWidgets.select_address(missing = orig_type, person = self.pat)
963 if adr is None:
964 if self.debug:
965 return _('no address type replacement selected')
966 return u''
967 adr_type = adr['address_type']
968 adr = self.pat.get_addresses(address_type = adr_type)[0]
969
970
971 template = _('%(street)s %(number)s, %(postcode)s %(urb)s, %(l10n_state)s, %(l10n_country)s')
972 if len(data_parts) > 1:
973 if data_parts[1].strip() != u'':
974 template = data_parts[1]
975
976 try:
977 return template % adr.fields_as_dict(escape_style = self.__esc_style)
978 except StandardError:
979 _log.exception('error formatting address')
980 _log.error('template: %s', template)
981
982 return None
983
985 requested_type = data.strip()
986 cache_key = 'adr-type-%s' % requested_type
987 try:
988 type2use = self.__cache[cache_key]
989 _log.debug('cache hit (%s): [%s] -> [%s]', cache_key, requested_type, type2use)
990 except KeyError:
991 type2use = requested_type
992 if type2use != u'':
993 adrs = self.pat.get_addresses(address_type = type2use)
994 if len(adrs) == 0:
995 _log.warning('no address of type [%s] for <%s> field extraction', requested_type, part)
996 type2use = u''
997 if type2use == u'':
998 _log.debug('asking user for replacement address type')
999 adr = gmPersonContactWidgets.select_address(missing = requested_type, person = self.pat)
1000 if adr is None:
1001 _log.debug('no replacement selected')
1002 if self.debug:
1003 return self._escape(_('no address type replacement selected'))
1004 return u''
1005 type2use = adr['address_type']
1006 self.__cache[cache_key] = type2use
1007 _log.debug('caching (%s): [%s] -> [%s]', cache_key, requested_type, type2use)
1008
1009 return self._escape(self.pat.get_addresses(address_type = type2use)[0][part])
1010
1012 return self.__get_variant_adr_part(data = data, part = 'street')
1013
1015 return self.__get_variant_adr_part(data = data, part = 'number')
1016
1018 return self.__get_variant_adr_part(data = data, part = 'subunit')
1019
1021 return self.__get_variant_adr_part(data = data, part = 'urb')
1022
1024 return self.__get_variant_adr_part(data = data, part = 'suburb')
1025
1026 - def _get_variant_adr_postcode(self, data=u'?'):
1027 return self.__get_variant_adr_part(data = data, part = 'postcode')
1028
1030 return self.__get_variant_adr_part(data = data, part = 'l10n_state')
1031
1033 return self.__get_variant_adr_part(data = data, part = 'l10n_country')
1034
1036 comm_type = None
1037 template = u'%(url)s'
1038 if data is not None:
1039 data_parts = data.split(u'//')
1040 if len(data_parts) > 0:
1041 comm_type = data_parts[0]
1042 if len(data_parts) > 1:
1043 template = data_parts[1]
1044
1045 comms = self.pat.get_comm_channels(comm_medium = comm_type)
1046 if len(comms) == 0:
1047 if self.debug:
1048 return template + u': ' + self._escape(_('no URL for comm channel [%s]') % data)
1049 return u''
1050
1051 return template % comms[0].fields_as_dict(escape_style = self.__esc_style)
1052
1053
1055
1056 template = u'%s'
1057 target_mime = None
1058 target_ext = None
1059 if data is not None:
1060 parts = data.split(u'//')
1061 template = parts[0]
1062 if len(parts) > 1:
1063 target_mime = parts[1].strip()
1064 if len(parts) > 2:
1065 target_ext = parts[2].strip()
1066 if target_ext is None:
1067 if target_mime is not None:
1068 target_ext = gmMimeLib.guess_ext_by_mimetype(mimetype = target_mime)
1069
1070 mugshot = self.pat.document_folder.latest_mugshot
1071 if mugshot is None:
1072 if self.debug:
1073 return self._escape(_('no mugshot available'))
1074 return u''
1075
1076 fname = mugshot.export_to_file (
1077 target_mime = target_mime,
1078 target_extension = target_ext,
1079 ignore_conversion_problems = True
1080 )
1081 if fname is None:
1082 if self.debug:
1083 return self._escape(_('cannot export or convert latest mugshot'))
1084 return u''
1085
1086 return template % fname
1087
1104
1105
1106
1107
1109 data_parts = data.split(u'//')
1110 if len(data_parts) < 2:
1111 return self._escape(u'current provider external ID: template is missing')
1112
1113 id_type = data_parts[0].strip()
1114 if id_type == u'':
1115 return self._escape(u'current provider external ID: type is missing')
1116
1117 issuer = data_parts[1].strip()
1118 if issuer == u'':
1119 return self._escape(u'current provider external ID: issuer is missing')
1120
1121 prov = gmStaff.gmCurrentProvider()
1122 ids = prov.identity.get_external_ids(id_type = id_type, issuer = issuer)
1123
1124 if len(ids) == 0:
1125 if self.debug:
1126 return self._escape(_('no external ID [%s] by [%s]') % (id_type, issuer))
1127 return u''
1128
1129 return self._escape(ids[0]['value'])
1130
1132 data_parts = data.split(u'//')
1133 if len(data_parts) < 2:
1134 return self._escape(u'primary in-praxis provider external ID: template is missing')
1135
1136 id_type = data_parts[0].strip()
1137 if id_type == u'':
1138 return self._escape(u'primary in-praxis provider external ID: type is missing')
1139
1140 issuer = data_parts[1].strip()
1141 if issuer == u'':
1142 return self._escape(u'primary in-praxis provider external ID: issuer is missing')
1143
1144 prov = self.pat.primary_provider
1145 if prov is None:
1146 if self.debug:
1147 return self._escape(_('no primary in-praxis provider'))
1148 return u''
1149
1150 ids = prov.identity.get_external_ids(id_type = id_type, issuer = issuer)
1151
1152 if len(ids) == 0:
1153 if self.debug:
1154 return self._escape(_('no external ID [%s] by [%s]') % (id_type, issuer))
1155 return u''
1156
1157 return self._escape(ids[0]['value'])
1158
1160 data_parts = data.split(u'//')
1161 if len(data_parts) < 2:
1162 return self._escape(u'patient external ID: template is missing')
1163
1164 id_type = data_parts[0].strip()
1165 if id_type == u'':
1166 return self._escape(u'patient external ID: type is missing')
1167
1168 issuer = data_parts[1].strip()
1169 if issuer == u'':
1170 return self._escape(u'patient external ID: issuer is missing')
1171
1172 ids = self.pat.get_external_ids(id_type = id_type, issuer = issuer)
1173
1174 if len(ids) == 0:
1175 if self.debug:
1176 return self._escape(_('no external ID [%s] by [%s]') % (id_type, issuer))
1177 return u''
1178
1179 return self._escape(ids[0]['value'])
1180
1182 if data is None:
1183 return self._escape(_('template is missing'))
1184
1185 template, separator = data.split('//', 2)
1186
1187 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() ])
1188
1195
1223
1232
1241
1249
1251
1252 template = u''
1253 date_format = '%Y %b %d %H:%M'
1254 separator = u'\n'
1255
1256 options = data.split(u'//')
1257 try:
1258 template = options[0].strip()
1259 date_format = options[1]
1260 separator = options[2]
1261 except IndexError:
1262 pass
1263
1264 if date_format.strip() == u'':
1265 date_format = '%Y %b %d %H:%M'
1266 if separator.strip() == u'':
1267 separator = u'\n'
1268
1269 results = gmMeasurementWidgets.manage_measurements(single_selection = False, emr = self.pat.emr)
1270 if results is None:
1271 if self.debug:
1272 return self._escape(_('no results for this patient (available or selected)'))
1273 return u''
1274
1275 if template == u'':
1276 return (separator + separator).join([ self._escape(r.format(date_format = date_format)) for r in results ])
1277
1278 return separator.join([ template % r.fields_as_dict(date_format = date_format, escape_style = self.__esc_style) for r in results ])
1279
1287
1289 options = data.split('//')
1290 template = options[0]
1291 if len(options) > 1:
1292 date_format = options[1]
1293 else:
1294 date_format = u'%Y %b %d'
1295
1296 vaccs = self.pat.emr.get_vaccinations(order_by = u'date_given DESC, vaccine')
1297
1298 return u'\n'.join([ template % v.fields_as_dict(date_format = date_format, escape_style = self.__esc_style) for v in vaccs ])
1299
1301
1302 if data is None:
1303 if self.debug:
1304 _log.error('PHX: missing placeholder arguments')
1305 return self._escape(_('PHX: Invalid placeholder options.'))
1306 return u''
1307
1308 _log.debug('arguments: %s', data)
1309
1310 data_parts = data.split(u'//')
1311 template = u'%s'
1312 separator = u'\n'
1313 date_format = '%Y %b %d'
1314 esc_style = None
1315 try:
1316 template = data_parts[0]
1317 separator = data_parts[1]
1318 date_format = data_parts[2]
1319 esc_style = data_parts[3]
1320 except IndexError:
1321 pass
1322
1323 phxs = gmEMRStructWidgets.select_health_issues(emr = self.pat.emr)
1324 if phxs is None:
1325 if self.debug:
1326 return self._escape(_('no PHX for this patient (available or selected)'))
1327 return u''
1328
1329 return separator.join ([
1330 template % phx.fields_as_dict (
1331 date_format = date_format,
1332
1333 escape_style = self.__esc_style,
1334 bool_strings = (self._escape(_('yes')), self._escape(_('no')))
1335 ) for phx in phxs
1336 ])
1337
1339
1340 if data is None:
1341 return self._escape(_('template is missing'))
1342
1343 probs = self.pat.emr.get_problems()
1344
1345 return u'\n'.join([ data % p.fields_as_dict(date_format = '%Y %b %d', escape_style = self.__esc_style) for p in probs ])
1346
1349
1352
1353 - def _get_variant_text_snippet(self, data=None):
1354 data_parts = data.split(u'//')
1355 keyword = data_parts[0]
1356 template = u'%s'
1357 if len(data_parts) > 1:
1358 template = data_parts[1]
1359
1360 expansion = gmKeywordExpansionWidgets.expand_keyword(keyword = keyword, show_list = True)
1361
1362 if expansion is None:
1363 if self.debug:
1364 return self._escape(_('no textual expansion found for keyword <%s>') % keyword)
1365 return u''
1366
1367
1368 return template % expansion
1369
1425
1426 - def _get_variant_free_text(self, data=u'tex//'):
1427
1428
1429
1430
1431 data_parts = data.split('//')
1432 format = data_parts[0]
1433 if len(data_parts) > 1:
1434 msg = data_parts[1]
1435 else:
1436 msg = _('generic text')
1437
1438 dlg = gmGuiHelpers.cMultilineTextEntryDlg (
1439 None,
1440 -1,
1441 title = _('Replacing <free_text> placeholder'),
1442 msg = _('Below you can enter free text.\n\n [%s]') % msg
1443 )
1444 dlg.enable_user_formatting = True
1445 decision = dlg.ShowModal()
1446
1447 if decision != wx.ID_SAVE:
1448 dlg.Destroy()
1449 if self.debug:
1450 return self._escape(_('Text input cancelled by user.'))
1451 return u''
1452
1453 text = dlg.value.strip()
1454 if dlg.is_user_formatted:
1455 dlg.Destroy()
1456 return text
1457
1458 dlg.Destroy()
1459
1460
1461
1462
1463
1464 return self._escape(text)
1465
1479
1493
1494
1495
1497 if self.__esc_func is None:
1498 return text
1499 return self.__esc_func(text)
1500
1502 """Functions a macro can legally use.
1503
1504 An instance of this class is passed to the GNUmed scripting
1505 listener. Hence, all actions a macro can legally take must
1506 be defined in this class. Thus we achieve some screening for
1507 security and also thread safety handling.
1508 """
1509
1510 - def __init__(self, personality = None):
1511 if personality is None:
1512 raise gmExceptions.ConstructorError, 'must specify personality'
1513 self.__personality = personality
1514 self.__attached = 0
1515 self._get_source_personality = None
1516 self.__user_done = False
1517 self.__user_answer = 'no answer yet'
1518 self.__pat = gmPerson.gmCurrentPatient()
1519
1520 self.__auth_cookie = str(random.random())
1521 self.__pat_lock_cookie = str(random.random())
1522 self.__lock_after_load_cookie = str(random.random())
1523
1524 _log.info('slave mode personality is [%s]', personality)
1525
1526
1527
1528 - def attach(self, personality = None):
1529 if self.__attached:
1530 _log.error('attach with [%s] rejected, already serving a client', personality)
1531 return (0, _('attach rejected, already serving a client'))
1532 if personality != self.__personality:
1533 _log.error('rejecting attach to personality [%s], only servicing [%s]' % (personality, self.__personality))
1534 return (0, _('attach to personality [%s] rejected') % personality)
1535 self.__attached = 1
1536 self.__auth_cookie = str(random.random())
1537 return (1, self.__auth_cookie)
1538
1539 - def detach(self, auth_cookie=None):
1540 if not self.__attached:
1541 return 1
1542 if auth_cookie != self.__auth_cookie:
1543 _log.error('rejecting detach() with cookie [%s]' % auth_cookie)
1544 return 0
1545 self.__attached = 0
1546 return 1
1547
1549 if not self.__attached:
1550 return 1
1551 self.__user_done = False
1552
1553 wx.CallAfter(self._force_detach)
1554 return 1
1555
1557 ver = _cfg.get(option = u'client_version')
1558 return "GNUmed %s, %s $Revision: 1.51 $" % (ver, self.__class__.__name__)
1559
1561 """Shuts down this client instance."""
1562 if not self.__attached:
1563 return 0
1564 if auth_cookie != self.__auth_cookie:
1565 _log.error('non-authenticated shutdown_gnumed()')
1566 return 0
1567 wx.CallAfter(self._shutdown_gnumed, forced)
1568 return 1
1569
1571 """Raise ourselves to the top of the desktop."""
1572 if not self.__attached:
1573 return 0
1574 if auth_cookie != self.__auth_cookie:
1575 _log.error('non-authenticated raise_gnumed()')
1576 return 0
1577 return "cMacroPrimitives.raise_gnumed() not implemented"
1578
1580 if not self.__attached:
1581 return 0
1582 if auth_cookie != self.__auth_cookie:
1583 _log.error('non-authenticated get_loaded_plugins()')
1584 return 0
1585 gb = gmGuiBroker.GuiBroker()
1586 return gb['horstspace.notebook.gui'].keys()
1587
1589 """Raise a notebook plugin within GNUmed."""
1590 if not self.__attached:
1591 return 0
1592 if auth_cookie != self.__auth_cookie:
1593 _log.error('non-authenticated raise_notebook_plugin()')
1594 return 0
1595
1596 wx.CallAfter(gmPlugin.raise_notebook_plugin, a_plugin)
1597 return 1
1598
1600 """Load external patient, perhaps create it.
1601
1602 Callers must use get_user_answer() to get status information.
1603 It is unsafe to proceed without knowing the completion state as
1604 the controlled client may be waiting for user input from a
1605 patient selection list.
1606 """
1607 if not self.__attached:
1608 return (0, _('request rejected, you are not attach()ed'))
1609 if auth_cookie != self.__auth_cookie:
1610 _log.error('non-authenticated load_patient_from_external_source()')
1611 return (0, _('rejected load_patient_from_external_source(), not authenticated'))
1612 if self.__pat.locked:
1613 _log.error('patient is locked, cannot load from external source')
1614 return (0, _('current patient is locked'))
1615 self.__user_done = False
1616 wx.CallAfter(self._load_patient_from_external_source)
1617 self.__lock_after_load_cookie = str(random.random())
1618 return (1, self.__lock_after_load_cookie)
1619
1621 if not self.__attached:
1622 return (0, _('request rejected, you are not attach()ed'))
1623 if auth_cookie != self.__auth_cookie:
1624 _log.error('non-authenticated lock_load_patient()')
1625 return (0, _('rejected lock_load_patient(), not authenticated'))
1626
1627 if lock_after_load_cookie != self.__lock_after_load_cookie:
1628 _log.warning('patient lock-after-load request rejected due to wrong cookie [%s]' % lock_after_load_cookie)
1629 return (0, 'patient lock-after-load request rejected, wrong cookie provided')
1630 self.__pat.locked = True
1631 self.__pat_lock_cookie = str(random.random())
1632 return (1, self.__pat_lock_cookie)
1633
1635 if not self.__attached:
1636 return (0, _('request rejected, you are not attach()ed'))
1637 if auth_cookie != self.__auth_cookie:
1638 _log.error('non-authenticated lock_into_patient()')
1639 return (0, _('rejected lock_into_patient(), not authenticated'))
1640 if self.__pat.locked:
1641 _log.error('patient is already locked')
1642 return (0, _('already locked into a patient'))
1643 searcher = gmPersonSearch.cPatientSearcher_SQL()
1644 if type(search_params) == types.DictType:
1645 idents = searcher.get_identities(search_dict=search_params)
1646 raise StandardError("must use dto, not search_dict")
1647 else:
1648 idents = searcher.get_identities(search_term=search_params)
1649 if idents is None:
1650 return (0, _('error searching for patient with [%s]/%s') % (search_term, search_dict))
1651 if len(idents) == 0:
1652 return (0, _('no patient found for [%s]/%s') % (search_term, search_dict))
1653
1654 if len(idents) > 1:
1655 return (0, _('several matching patients found for [%s]/%s') % (search_term, search_dict))
1656 if not gmPatSearchWidgets.set_active_patient(patient = idents[0]):
1657 return (0, _('cannot activate patient [%s] (%s/%s)') % (str(idents[0]), search_term, search_dict))
1658 self.__pat.locked = True
1659 self.__pat_lock_cookie = str(random.random())
1660 return (1, self.__pat_lock_cookie)
1661
1663 if not self.__attached:
1664 return (0, _('request rejected, you are not attach()ed'))
1665 if auth_cookie != self.__auth_cookie:
1666 _log.error('non-authenticated unlock_patient()')
1667 return (0, _('rejected unlock_patient, not authenticated'))
1668
1669 if not self.__pat.locked:
1670 return (1, '')
1671
1672 if unlock_cookie != self.__pat_lock_cookie:
1673 _log.warning('patient unlock request rejected due to wrong cookie [%s]' % unlock_cookie)
1674 return (0, 'patient unlock request rejected, wrong cookie provided')
1675 self.__pat.locked = False
1676 return (1, '')
1677
1679 if not self.__attached:
1680 return 0
1681 if auth_cookie != self.__auth_cookie:
1682 _log.error('non-authenticated select_identity()')
1683 return 0
1684 return "cMacroPrimitives.assume_staff_identity() not implemented"
1685
1687 if not self.__user_done:
1688 return (0, 'still waiting')
1689 self.__user_done = False
1690 return (1, self.__user_answer)
1691
1692
1693
1695 msg = _(
1696 'Someone tries to forcibly break the existing\n'
1697 'controlling connection. This may or may not\n'
1698 'have legitimate reasons.\n\n'
1699 'Do you want to allow breaking the connection ?'
1700 )
1701 can_break_conn = gmGuiHelpers.gm_show_question (
1702 aMessage = msg,
1703 aTitle = _('forced detach attempt')
1704 )
1705 if can_break_conn:
1706 self.__user_answer = 1
1707 else:
1708 self.__user_answer = 0
1709 self.__user_done = True
1710 if can_break_conn:
1711 self.__pat.locked = False
1712 self.__attached = 0
1713 return 1
1714
1716 top_win = wx.GetApp().GetTopWindow()
1717 if forced:
1718 top_win.Destroy()
1719 else:
1720 top_win.Close()
1721
1730
1731
1732
1733 if __name__ == '__main__':
1734
1735 if len(sys.argv) < 2:
1736 sys.exit()
1737
1738 if sys.argv[1] != 'test':
1739 sys.exit()
1740
1741 gmI18N.activate_locale()
1742 gmI18N.install_domain()
1743
1744
1746 handler = gmPlaceholderHandler()
1747 handler.debug = True
1748
1749 for placeholder in ['a', 'b']:
1750 print handler[placeholder]
1751
1752 pat = gmPersonSearch.ask_for_patient()
1753 if pat is None:
1754 return
1755
1756 gmPatSearchWidgets.set_active_patient(patient = pat)
1757
1758 print 'DOB (YYYY-MM-DD):', handler['date_of_birth::%Y-%m-%d']
1759
1760 app = wx.PyWidgetTester(size = (200, 50))
1761 for placeholder in known_placeholders:
1762 print placeholder, "=", handler[placeholder]
1763
1764 ph = 'progress_notes::ap'
1765 print '%s: %s' % (ph, handler[ph])
1766
1768
1769 tests = [
1770
1771 '$<lastname>$',
1772 '$<lastname::::3>$',
1773 '$<name::%(title)s %(firstnames)s%(preferred)s%(lastnames)s>$',
1774
1775
1776 'lastname',
1777 '$<lastname',
1778 '$<lastname::',
1779 '$<lastname::>$',
1780 '$<lastname::abc>$',
1781 '$<lastname::abc::>$',
1782 '$<lastname::abc::3>$',
1783 '$<lastname::abc::xyz>$',
1784 '$<lastname::::>$',
1785 '$<lastname::::xyz>$',
1786
1787 '$<date_of_birth::%Y-%m-%d>$',
1788 '$<date_of_birth::%Y-%m-%d::3>$',
1789 '$<date_of_birth::%Y-%m-%d::>$',
1790
1791
1792 '$<adr_location::home::35>$',
1793 '$<gender_mapper::male//female//other::5>$',
1794 '$<current_meds::==> %(brand)s %(preparation)s (%(substance)s) <==\n::50>$',
1795 '$<allergy_list::%(descriptor)s, >$',
1796 '$<current_meds_table::latex//by-brand>$'
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811 ]
1812
1813
1814
1815
1816
1817 pat = gmPersonSearch.ask_for_patient()
1818 if pat is None:
1819 return
1820
1821 gmPatSearchWidgets.set_active_patient(patient = pat)
1822
1823 handler = gmPlaceholderHandler()
1824 handler.debug = True
1825
1826 for placeholder in tests:
1827 print placeholder, "=>", handler[placeholder]
1828 print "--------------"
1829 raw_input()
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1842 from Gnumed.pycommon import gmScriptingListener
1843 import xmlrpclib
1844 listener = gmScriptingListener.cScriptingListener(macro_executor = cMacroPrimitives(personality='unit test'), port=9999)
1845
1846 s = xmlrpclib.ServerProxy('http://localhost:9999')
1847 print "should fail:", s.attach()
1848 print "should fail:", s.attach('wrong cookie')
1849 print "should work:", s.version()
1850 print "should fail:", s.raise_gnumed()
1851 print "should fail:", s.raise_notebook_plugin('test plugin')
1852 print "should fail:", s.lock_into_patient('kirk, james')
1853 print "should fail:", s.unlock_patient()
1854 status, conn_auth = s.attach('unit test')
1855 print "should work:", status, conn_auth
1856 print "should work:", s.version()
1857 print "should work:", s.raise_gnumed(conn_auth)
1858 status, pat_auth = s.lock_into_patient(conn_auth, 'kirk, james')
1859 print "should work:", status, pat_auth
1860 print "should fail:", s.unlock_patient(conn_auth, 'bogus patient unlock cookie')
1861 print "should work", s.unlock_patient(conn_auth, pat_auth)
1862 data = {'firstname': 'jame', 'lastnames': 'Kirk', 'gender': 'm'}
1863 status, pat_auth = s.lock_into_patient(conn_auth, data)
1864 print "should work:", status, pat_auth
1865 print "should work", s.unlock_patient(conn_auth, pat_auth)
1866 print s.detach('bogus detach cookie')
1867 print s.detach(conn_auth)
1868 del s
1869
1870 listener.shutdown()
1871
1873
1874 import re as regex
1875
1876 tests = [
1877 ' $<lastname>$ ',
1878 ' $<lastname::::3>$ ',
1879
1880
1881 '$<date_of_birth::%Y-%m-%d>$',
1882 '$<date_of_birth::%Y-%m-%d::3>$',
1883 '$<date_of_birth::%Y-%m-%d::>$',
1884
1885 '$<adr_location::home::35>$',
1886 '$<gender_mapper::male//female//other::5>$',
1887 '$<current_meds::==> %(brand)s %(preparation)s (%(substance)s) <==\\n::50>$',
1888 '$<allergy_list::%(descriptor)s, >$',
1889
1890 '\\noindent Patient: $<lastname>$, $<firstname>$',
1891 '$<allergies::%(descriptor)s & %(l10n_type)s & {\\footnotesize %(reaction)s} \tabularnewline \hline >$',
1892 '$<current_meds:: \item[%(substance)s] {\\footnotesize (%(brand)s)} %(preparation)s %(amount)s%(unit)s: %(schedule)s >$'
1893 ]
1894
1895 tests = [
1896
1897 'junk $<lastname::::3>$ junk',
1898 'junk $<lastname::abc::3>$ junk',
1899 'junk $<lastname::abc>$ junk',
1900 'junk $<lastname>$ junk',
1901
1902 'junk $<lastname>$ junk $<firstname>$ junk',
1903 'junk $<lastname::abc>$ junk $<fiststname::abc>$ junk',
1904 'junk $<lastname::abc::3>$ junk $<firstname::abc::3>$ junk',
1905 'junk $<lastname::::3>$ junk $<firstname::::3>$ junk'
1906
1907 ]
1908
1909 print "testing placeholder regex:", default_placeholder_regex
1910 print ""
1911
1912 for t in tests:
1913 print 'line: "%s"' % t
1914 print "placeholders:"
1915 for p in regex.findall(default_placeholder_regex, t, regex.IGNORECASE):
1916 print ' => "%s"' % p
1917 print " "
1918
1979
1980
1988
1991
1992
1993
1994
1995
1996
1997
1998 test_placeholder()
1999
2000
2001
2002