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
198 u'current_provider_external_id',
199 u'primary_praxis_provider_external_id',
200
201
202 u'bill',
203 u'bill_item'
204 ]
205
206
207 default_placeholder_regex = r'\$<.+?>\$'
208
209
210 default_placeholder_start = u'$<'
211 default_placeholder_end = u'>$'
212
214 fname = gmTools.get_unique_filename(prefix = 'gm-placeholders-', suffix = '.txt')
215 ph_file = codecs.open(filename = fname, mode = 'wb', encoding = 'utf8', errors = 'replace')
216
217 ph_file.write(u'Here you can find some more documentation on placeholder use:\n')
218 ph_file.write(u'\n http://wiki.gnumed.de/bin/view/Gnumed/GmManualLettersForms\n\n\n')
219
220 ph_file.write(u'Simple placeholders (use like: $<PLACEHOLDER_NAME>$):\n')
221 for ph in known_placeholders:
222 ph_file.write(u' %s\n' % ph)
223 ph_file.write(u'\n')
224
225 ph_file.write(u'Variable placeholders (use like: $<PLACEHOLDER_NAME::ARGUMENTS::MAX OUTPUT LENGTH>$):\n')
226 for ph in known_variant_placeholders:
227 ph_file.write(u' %s\n' % ph)
228 ph_file.write(u'\n')
229
230 ph_file.write(u'Injectable placeholders (use like: $<PLACEHOLDER_NAME::ARGUMENTS::MAX OUTPUT LENGTH>$):\n')
231 for ph in _injectable_placeholders:
232 ph_file.write(u' %s\n' % ph)
233 ph_file.write(u'\n')
234
235 ph_file.close()
236 gmMimeLib.call_viewer_on_file(aFile = fname, block = False)
237
239 """Returns values for placeholders.
240
241 - patient related placeholders operate on the currently active patient
242 - is passed to the forms handling code, for example
243
244 Return values when .debug is False:
245 - errors with placeholders return None
246 - placeholders failing to resolve to a value return an empty string
247
248 Return values when .debug is True:
249 - errors with placeholders return an error string
250 - placeholders failing to resolve to a value return a warning string
251
252 There are several types of placeholders:
253
254 simple static placeholders
255 - those are listed in known_placeholders
256 - they are used as-is
257
258 extended static placeholders
259 - those are, effectively, static placeholders
260 with a maximum length attached (after "::::")
261
262 injectable placeholders
263 - they must be set up before use by set_placeholder()
264 - they should be removed after use by unset_placeholder()
265 - the syntax is like extended static placeholders
266 - they are listed in _injectable_placeholders
267
268 variant placeholders
269 - those are listed in known_variant_placeholders
270 - they are parsed into placeholder, data, and maximum length
271 - the length is optional
272 - data is passed to the handler
273
274 Note that this cannot be called from a non-gui thread unless
275 wrapped in wx.CallAfter().
276 """
278
279 self.pat = gmPerson.gmCurrentPatient()
280 self.debug = False
281
282 self.invalid_placeholder_template = _('invalid placeholder [%s]')
283
284 self.__cache = {}
285
286 self.__esc_style = None
287 self.__esc_func = lambda x:x
288
289
290
294
298
300 self.__cache[key] = value
301
303 del self.__cache[key]
304
308
309 escape_style = property(lambda x:x, _set_escape_style)
310
319
320 escape_function = property(lambda x:x, _set_escape_function)
321
322
323
325 """Map self['placeholder'] to self.placeholder.
326
327 This is useful for replacing placeholders parsed out
328 of documents as strings.
329
330 Unknown/invalid placeholders still deliver a result but
331 it will be glaringly obvious if debugging is enabled.
332 """
333 _log.debug('replacing [%s]', placeholder)
334
335 original_placeholder = placeholder
336
337 if placeholder.startswith(default_placeholder_start):
338 placeholder = placeholder[len(default_placeholder_start):]
339 if placeholder.endswith(default_placeholder_end):
340 placeholder = placeholder[:-len(default_placeholder_end)]
341 else:
342 _log.debug('placeholder must either start with [%s] and end with [%s] or neither of both', default_placeholder_start, default_placeholder_end)
343 if self.debug:
344 return self.invalid_placeholder_template % original_placeholder
345 return None
346
347
348 if placeholder in known_placeholders:
349 return getattr(self, placeholder)
350
351
352 parts = placeholder.split('::::', 1)
353 if len(parts) == 2:
354 name, lng = parts
355 unknown_injectable = False
356 try:
357 val = _injectable_placeholders[name]
358 except KeyError:
359 unknown_injectable = True
360 except:
361 _log.exception('placeholder handling error: %s', original_placeholder)
362 if self.debug:
363 return self.invalid_placeholder_template % original_placeholder
364 return None
365 if not unknown_injectable:
366 if val is None:
367 if self.debug:
368 return u'injectable placeholder [%s]: no value available' % name
369 return placeholder
370 return val[:int(lng)]
371
372
373 parts = placeholder.split('::::', 1)
374 if len(parts) == 2:
375 name, lng = parts
376 try:
377 return getattr(self, name)[:int(lng)]
378 except:
379 _log.exception('placeholder handling error: %s', original_placeholder)
380 if self.debug:
381 return self.invalid_placeholder_template % original_placeholder
382 return None
383
384
385 parts = placeholder.split('::')
386
387 if len(parts) == 1:
388 _log.warning('invalid placeholder layout: %s', original_placeholder)
389 if self.debug:
390 return self.invalid_placeholder_template % original_placeholder
391 return None
392
393 if len(parts) == 2:
394 name, data = parts
395 lng = None
396
397 if len(parts) == 3:
398 name, data, lng = parts
399 try:
400 lng = int(lng)
401 except (TypeError, ValueError):
402 _log.error('placeholder length definition error: %s, discarding length: >%s<', original_placeholder, lng)
403 lng = None
404
405 if len(parts) > 3:
406 _log.warning('invalid placeholder layout: %s', original_placeholder)
407 if self.debug:
408 return self.invalid_placeholder_template % original_placeholder
409 return None
410
411 handler = getattr(self, '_get_variant_%s' % name, None)
412 if handler is None:
413 _log.warning('no handler <_get_variant_%s> for placeholder %s', name, original_placeholder)
414 if self.debug:
415 return self.invalid_placeholder_template % original_placeholder
416 return None
417
418 try:
419 if lng is None:
420 return handler(data = data)
421 return handler(data = data)[:lng]
422 except:
423 _log.exception('placeholder handling error: %s', original_placeholder)
424 if self.debug:
425 return self.invalid_placeholder_template % original_placeholder
426 return None
427
428 _log.error('something went wrong, should never get here')
429 return None
430
431
432
433
434
436 """This does nothing, used as a NOOP properties setter."""
437 pass
438
441
444
447
449 return self._get_variant_date_of_birth(data = '%x')
450
452 return self._get_variant_soap()
453
455 return self._get_variant_soap(data = u's')
456
458 return self._get_variant_soap(data = u'o')
459
461 return self._get_variant_soap(data = u'a')
462
464 return self._get_variant_soap(data = u'p')
465
467 return self._get_variant_soap(data = u'u')
468
470 return self._get_variant_soap(soap_cats = None)
471
473 return self._escape (
474 gmTools.coalesce (
475 _cfg.get(option = u'client_version'),
476 u'%s' % self.__class__.__name__
477 )
478 )
479
496
511
513 allg_state = self.pat.get_emr().allergy_state
514
515 if allg_state['last_confirmed'] is None:
516 date_confirmed = u''
517 else:
518 date_confirmed = u' (%s)' % gmDateTime.pydt_strftime (
519 allg_state['last_confirmed'],
520 format = '%Y %B %d'
521 )
522
523 tmp = u'%s%s' % (
524 allg_state.state_string,
525 date_confirmed
526 )
527 return self._escape(tmp)
528
529
530
531 placeholder_regex = property(lambda x: default_placeholder_regex, _setter_noop)
532
533
534
535
536 lastname = property(_get_lastname, _setter_noop)
537 firstname = property(_get_firstname, _setter_noop)
538 title = property(_get_title, _setter_noop)
539 date_of_birth = property(_get_dob, _setter_noop)
540
541 progress_notes = property(_get_progress_notes, _setter_noop)
542 soap = property(_get_progress_notes, _setter_noop)
543 soap_s = property(_get_soap_s, _setter_noop)
544 soap_o = property(_get_soap_o, _setter_noop)
545 soap_a = property(_get_soap_a, _setter_noop)
546 soap_p = property(_get_soap_p, _setter_noop)
547 soap_u = property(_get_soap_u, _setter_noop)
548 soap_admin = property(_get_soap_admin, _setter_noop)
549
550 allergy_state = property(_get_allergy_state, _setter_noop)
551
552 client_version = property(_get_client_version, _setter_noop)
553
554 current_provider = property(_get_current_provider, _setter_noop)
555 primary_praxis_provider = property(_get_primary_praxis_provider, _setter_noop)
556
557
558
560
561 select = False
562 include_descriptions = False
563 template = u'%s'
564 path_template = None
565 export_path = None
566
567 data_parts = data.split('//')
568
569 if u'select' in data_parts:
570 select = True
571 data_parts.remove(u'select')
572
573 if u'description' in data_parts:
574 include_descriptions = True
575 data_parts.remove(u'description')
576
577 template = data_parts[0]
578
579 if len(data_parts) > 1:
580 path_template = data_parts[1]
581
582 if len(data_parts) > 2:
583 export_path = data_parts[2]
584
585
586 if export_path is not None:
587 export_path = os.path.normcase(os.path.expanduser(export_path))
588 gmTools.mkdir(export_path)
589
590
591 if select:
592 docs = gmDocumentWidgets.manage_documents(msg = _('Select the patient documents to reference from the new document.'), single_selection = False)
593 else:
594 docs = self.pat.document_folder.documents
595
596 if docs is None:
597 return u''
598
599 lines = []
600 for doc in docs:
601 lines.append(template % doc.fields_as_dict(date_format = '%Y %b %d', escape_style = self.__esc_style))
602 if include_descriptions:
603 for desc in doc.get_descriptions(max_lng = None):
604 lines.append(self._escape(desc['text'] + u'\n'))
605 if path_template is not None:
606 for part_name in doc.export_parts_to_files(export_dir = export_path):
607 path, name = os.path.split(part_name)
608 lines.append(path_template % {'fullpath': part_name, 'name': name})
609
610 return u'\n'.join(lines)
611
613
614 encounters = gmEMRStructWidgets.select_encounters(single_selection = False)
615 if not encounters:
616 return u''
617
618 template = data
619
620 lines = []
621 for enc in encounters:
622 try:
623 lines.append(template % enc.fields_as_dict(date_format = '%Y %b %d', escape_style = self.__esc_style))
624 except:
625 lines.append(u'error formatting encounter')
626 _log.exception('problem formatting encounter list')
627 _log.error('template: %s', template)
628 _log.error('encounter: %s', encounter)
629
630 return u'\n'.join(lines)
631
633 """Select encounters from list and format SOAP thereof.
634
635 data: soap_cats (' ' -> None -> admin) // date format
636 """
637
638 cats = None
639 date_format = None
640
641 if data is not None:
642 data_parts = data.split('//')
643
644
645 if len(data_parts[0]) > 0:
646 cats = []
647 if u' ' in data_parts[0]:
648 cats.append(None)
649 data_parts[0] = data_parts[0].replace(u' ', u'')
650 cats.extend(list(data_parts[0]))
651
652
653 if len(data_parts) > 1:
654 if len(data_parts[1]) > 0:
655 date_format = data_parts[1]
656
657 encounters = gmEMRStructWidgets.select_encounters(single_selection = False)
658 if not encounters:
659 return u''
660
661 chunks = []
662 for enc in encounters:
663 chunks.append(enc.format_latex (
664 date_format = date_format,
665 soap_cats = cats,
666 soap_order = u'soap_rank, date'
667 ))
668
669 return u''.join(chunks)
670
672
673 cats = list(u'soapu')
674 cats.append(None)
675 template = u'%s'
676 interactive = True
677 line_length = 9999
678 target_format = None
679 time_range = None
680
681 if data is not None:
682 data_parts = data.split('//')
683
684
685 cats = []
686
687 for c in list(data_parts[0]):
688 if c == u' ':
689 c = None
690 cats.append(c)
691
692 if cats == u'':
693 cats = list(u'soapu').append(None)
694
695
696 if len(data_parts) > 1:
697 template = data_parts[1]
698
699
700 if len(data_parts) > 2:
701 try:
702 line_length = int(data_parts[2])
703 except:
704 line_length = 9999
705
706
707 if len(data_parts) > 3:
708 try:
709 time_range = 7 * int(data_parts[3])
710 except:
711
712
713 time_range = data_parts[3]
714
715
716 if len(data_parts) > 4:
717 target_format = data_parts[4]
718
719
720 narr = self.pat.emr.get_as_journal(soap_cats = cats, time_range = time_range)
721
722 if len(narr) == 0:
723 return u''
724
725 keys = narr[0].keys()
726 lines = []
727 line_dict = {}
728 for n in narr:
729 for key in keys:
730 if isinstance(n[key], basestring):
731 line_dict[key] = self._escape(text = n[key])
732 continue
733 line_dict[key] = n[key]
734 try:
735 lines.append((template % line_dict)[:line_length])
736 except KeyError:
737 return u'invalid key in template [%s], valid keys: %s]' % (template, str(keys))
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761 return u'\n'.join(lines)
762
764 return self.__get_variant_soap_by_issue_or_episode(data = data, mode = u'issue')
765
767 return self.__get_variant_soap_by_issue_or_episode(data = data, mode = u'episode')
768
770
771
772 cats = list(u'soapu')
773 cats.append(None)
774
775 date_format = None
776 template = u'%s'
777
778 if data is not None:
779 data_parts = data.split('//')
780
781
782 if len(data_parts[0]) > 0:
783 cats = []
784 if u' ' in data_parts[0]:
785 cats.append(None)
786 cats.extend(list(data_parts[0].replace(u' ', u'')))
787
788
789 if len(data_parts) > 1:
790 if len(data_parts[1]) > 0:
791 date_format = data_parts[1]
792
793
794 if len(data_parts) > 2:
795 if len(data_parts[2]) > 0:
796 template = data_parts[2]
797
798 if mode == u'issue':
799 narr = gmNarrativeWidgets.select_narrative_by_issue(soap_cats = cats)
800 else:
801 narr = gmNarrativeWidgets.select_narrative_by_episode(soap_cats = cats)
802
803 if narr is None:
804 return u''
805
806 if len(narr) == 0:
807 return u''
808
809 try:
810 narr = [ template % n.fields_as_dict(date_format = date_format, escape_style = self.__esc_style) for n in narr ]
811 except KeyError:
812 return u'invalid key in template [%s], valid keys: %s]' % (template, str(narr[0].keys()))
813
814 return u'\n'.join(narr)
815
817 return self._get_variant_soap(data=data)
818
820
821
822 cats = list(u'soapu')
823 cats.append(None)
824 template = u'%(narrative)s'
825
826 if data is not None:
827 data_parts = data.split('//')
828
829
830 cats = []
831
832 for cat in list(data_parts[0]):
833 if cat == u' ':
834 cat = None
835 cats.append(cat)
836
837 if cats == u'':
838 cats = list(u'soapu')
839 cats.append(None)
840
841
842 if len(data_parts) > 1:
843 template = data_parts[1]
844
845 narr = gmNarrativeWidgets.select_narrative_from_episodes(soap_cats = cats)
846
847 if narr is None:
848 return u''
849
850 if len(narr) == 0:
851 return u''
852
853
854
855
856 if u'%s' in template:
857 narr = [ self._escape(n['narrative']) for n in narr ]
858 else:
859 narr = [ n.fields_as_dict(escape_style = self.__esc_style) for n in narr ]
860
861 try:
862 narr = [ template % n for n in narr ]
863 except KeyError:
864 return u'invalid key in template [%s], valid keys: %s]' % (template, str(narr[0].keys()))
865 except TypeError:
866 return u'cannot mix "%%s" and "%%(key)s" in template [%s]' % template
867
868 return u'\n'.join(narr)
869
871 if data is None:
872 return [_('template is missing')]
873
874 name = self.pat.get_active_name()
875
876 parts = {
877 'title': self._escape(gmTools.coalesce(name['title'], u'')),
878 'firstnames': self._escape(name['firstnames']),
879 'lastnames': self._escape(name['lastnames']),
880 'preferred': self._escape(gmTools.coalesce (
881 initial = name['preferred'],
882 instead = u' ',
883 template_initial = u' "%s" '
884 ))
885 }
886
887 return data % parts
888
891
892
894
895 values = data.split('//', 2)
896
897 if len(values) == 2:
898 male_value, female_value = values
899 other_value = u'<unkown gender>'
900 elif len(values) == 3:
901 male_value, female_value, other_value = values
902 else:
903 return _('invalid gender mapping layout: [%s]') % data
904
905 if self.pat['gender'] == u'm':
906 return self._escape(male_value)
907
908 if self.pat['gender'] == u'f':
909 return self._escape(female_value)
910
911 return self._escape(other_value)
912
913
914
916
917 data_parts = data.split(u'//')
918
919
920 adr_type = data_parts[0].strip()
921 orig_type = adr_type
922 if adr_type != u'':
923 adrs = self.pat.get_addresses(address_type = adr_type)
924 if len(adrs) == 0:
925 _log.warning('no address for type [%s]', adr_type)
926 adr_type = u''
927 if adr_type == u'':
928 _log.debug('asking user for address type')
929 adr = gmPersonContactWidgets.select_address(missing = orig_type, person = self.pat)
930 if adr is None:
931 if self.debug:
932 return _('no address type replacement selected')
933 return u''
934 adr_type = adr['address_type']
935 adr = self.pat.get_addresses(address_type = adr_type)[0]
936
937
938 template = _('%(street)s %(number)s, %(postcode)s %(urb)s, %(l10n_state)s, %(l10n_country)s')
939 if len(data_parts) > 1:
940 if data_parts[1].strip() != u'':
941 template = data_parts[1]
942
943 try:
944 return template % adr.fields_as_dict(escape_style = self.__esc_style)
945 except StandardError:
946 _log.exception('error formatting address')
947 _log.error('template: %s', template)
948
949 return None
950
952 requested_type = data.strip()
953 cache_key = 'adr-type-%s' % requested_type
954 try:
955 type2use = self.__cache[cache_key]
956 _log.debug('cache hit (%s): [%s] -> [%s]', cache_key, requested_type, type2use)
957 except KeyError:
958 type2use = requested_type
959 if type2use != u'':
960 adrs = self.pat.get_addresses(address_type = type2use)
961 if len(adrs) == 0:
962 _log.warning('no address of type [%s] for <%s> field extraction', requested_type, part)
963 type2use = u''
964 if type2use == u'':
965 _log.debug('asking user for replacement address type')
966 adr = gmPersonContactWidgets.select_address(missing = requested_type, person = self.pat)
967 if adr is None:
968 _log.debug('no replacement selected')
969 if self.debug:
970 return self._escape(_('no address type replacement selected'))
971 return u''
972 type2use = adr['address_type']
973 self.__cache[cache_key] = type2use
974 _log.debug('caching (%s): [%s] -> [%s]', cache_key, requested_type, type2use)
975
976 return self._escape(self.pat.get_addresses(address_type = type2use)[0][part])
977
979 return self.__get_variant_adr_part(data = data, part = 'street')
980
982 return self.__get_variant_adr_part(data = data, part = 'number')
983
985 return self.__get_variant_adr_part(data = data, part = 'subunit')
986
988 return self.__get_variant_adr_part(data = data, part = 'urb')
989
991 return self.__get_variant_adr_part(data = data, part = 'suburb')
992
993 - def _get_variant_adr_postcode(self, data=u'?'):
994 return self.__get_variant_adr_part(data = data, part = 'postcode')
995
997 return self.__get_variant_adr_part(data = data, part = 'l10n_state')
998
1000 return self.__get_variant_adr_part(data = data, part = 'l10n_country')
1001
1003 comm_type = None
1004 template = u'%(url)s'
1005 if data is not None:
1006 data_parts = data.split(u'//')
1007 if len(data_parts) > 0:
1008 comm_type = data_parts[0]
1009 if len(data_parts) > 1:
1010 template = data_parts[1]
1011
1012 comms = self.pat.get_comm_channels(comm_medium = comm_type)
1013 if len(comms) == 0:
1014 if self.debug:
1015 return template + u': ' + self._escape(_('no URL for comm channel [%s]') % data)
1016 return u''
1017
1018 return template % comms[0].fields_as_dict(escape_style = self.__esc_style)
1019
1020
1022
1023 template = u'%s'
1024 target_mime = None
1025 target_ext = None
1026 if data is not None:
1027 parts = data.split(u'//')
1028 template = parts[0]
1029 if len(parts) > 1:
1030 target_mime = parts[1].strip()
1031 if len(parts) > 2:
1032 target_ext = parts[2].strip()
1033 if target_ext is None:
1034 if target_mime is not None:
1035 target_ext = gmMimeLib.guess_ext_by_mimetype(mimetype = target_mime)
1036
1037 mugshot = self.pat.document_folder.latest_mugshot
1038 if mugshot is None:
1039 if self.debug:
1040 return self._escape(_('no mugshot available'))
1041 return u''
1042
1043 fname = mugshot.export_to_file (
1044 target_mime = target_mime,
1045 target_extension = target_ext,
1046 ignore_conversion_problems = True
1047 )
1048 if fname is None:
1049 if self.debug:
1050 return self._escape(_('cannot export or convert latest mugshot'))
1051 return u''
1052
1053 return template % fname
1054
1071
1072
1073
1074
1076 data_parts = data.split(u'//')
1077 if len(data_parts) < 2:
1078 return self._escape(u'current provider external ID: template is missing')
1079
1080 id_type = data_parts[0].strip()
1081 if id_type == u'':
1082 return self._escape(u'current provider external ID: type is missing')
1083
1084 issuer = data_parts[1].strip()
1085 if issuer == u'':
1086 return self._escape(u'current provider external ID: issuer is missing')
1087
1088 prov = gmStaff.gmCurrentProvider()
1089 ids = prov.identity.get_external_ids(id_type = id_type, issuer = issuer)
1090
1091 if len(ids) == 0:
1092 if self.debug:
1093 return self._escape(_('no external ID [%s] by [%s]') % (id_type, issuer))
1094 return u''
1095
1096 return self._escape(ids[0]['value'])
1097
1099 data_parts = data.split(u'//')
1100 if len(data_parts) < 2:
1101 return self._escape(u'primary in-praxis provider external ID: template is missing')
1102
1103 id_type = data_parts[0].strip()
1104 if id_type == u'':
1105 return self._escape(u'primary in-praxis provider external ID: type is missing')
1106
1107 issuer = data_parts[1].strip()
1108 if issuer == u'':
1109 return self._escape(u'primary in-praxis provider external ID: issuer is missing')
1110
1111 prov = self.pat.primary_provider
1112 if prov is None:
1113 if self.debug:
1114 return self._escape(_('no primary in-praxis provider'))
1115 return u''
1116
1117 ids = prov.identity.get_external_ids(id_type = id_type, issuer = issuer)
1118
1119 if len(ids) == 0:
1120 if self.debug:
1121 return self._escape(_('no external ID [%s] by [%s]') % (id_type, issuer))
1122 return u''
1123
1124 return self._escape(ids[0]['value'])
1125
1127 data_parts = data.split(u'//')
1128 if len(data_parts) < 2:
1129 return self._escape(u'patient external ID: template is missing')
1130
1131 id_type = data_parts[0].strip()
1132 if id_type == u'':
1133 return self._escape(u'patient external ID: type is missing')
1134
1135 issuer = data_parts[1].strip()
1136 if issuer == u'':
1137 return self._escape(u'patient external ID: issuer is missing')
1138
1139 ids = self.pat.get_external_ids(id_type = id_type, issuer = issuer)
1140
1141 if len(ids) == 0:
1142 if self.debug:
1143 return self._escape(_('no external ID [%s] by [%s]') % (id_type, issuer))
1144 return u''
1145
1146 return self._escape(ids[0]['value'])
1147
1149 if data is None:
1150 return self._escape(_('template is missing'))
1151
1152 template, separator = data.split('//', 2)
1153
1154 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() ])
1155
1162
1190
1199
1208
1216
1218
1219 template = u''
1220 date_format = '%Y %b %d %H:%M'
1221 separator = u'\n'
1222
1223 options = data.split(u'//')
1224 try:
1225 template = options[0].strip()
1226 date_format = options[1]
1227 separator = options[2]
1228 except IndexError:
1229 pass
1230
1231 if date_format.strip() == u'':
1232 date_format = '%Y %b %d %H:%M'
1233 if separator.strip() == u'':
1234 separator = u'\n'
1235
1236 results = gmMeasurementWidgets.manage_measurements(single_selection = False, emr = self.pat.emr)
1237 if results is None:
1238 if self.debug:
1239 return self._escape(_('no results for this patient (available or selected)'))
1240 return u''
1241
1242 if template == u'':
1243 return (separator + separator).join([ self._escape(r.format(date_format = date_format)) for r in results ])
1244
1245 return separator.join([ template % r.fields_as_dict(date_format = date_format, escape_style = self.__esc_style) for r in results ])
1246
1254
1256 options = data.split('//')
1257 template = options[0]
1258 if len(options) > 1:
1259 date_format = options[1]
1260 else:
1261 date_format = u'%Y %b %d'
1262
1263 vaccs = self.pat.emr.get_vaccinations(order_by = u'date_given DESC, vaccine')
1264
1265 return u'\n'.join([ template % v.fields_as_dict(date_format = date_format, escape_style = self.__esc_style) for v in vaccs ])
1266
1268
1269 if data is None:
1270 if self.debug:
1271 _log.error('PHX: missing placeholder arguments')
1272 return self._escape(_('PHX: Invalid placeholder options.'))
1273 return u''
1274
1275 _log.debug('arguments: %s', data)
1276
1277 data_parts = data.split(u'//')
1278 template = u'%s'
1279 separator = u'\n'
1280 date_format = '%Y %b %d'
1281 esc_style = None
1282 try:
1283 template = data_parts[0]
1284 separator = data_parts[1]
1285 date_format = data_parts[2]
1286 esc_style = data_parts[3]
1287 except IndexError:
1288 pass
1289
1290 phxs = gmEMRStructWidgets.select_health_issues(emr = self.pat.emr)
1291 if phxs is None:
1292 if self.debug:
1293 return self._escape(_('no PHX for this patient (available or selected)'))
1294 return u''
1295
1296 return separator.join ([
1297 template % phx.fields_as_dict (
1298 date_format = date_format,
1299
1300 escape_style = self.__esc_style,
1301 bool_strings = (self._escape(_('yes')), self._escape(_('no')))
1302 ) for phx in phxs
1303 ])
1304
1306
1307 if data is None:
1308 return self._escape(_('template is missing'))
1309
1310 probs = self.pat.emr.get_problems()
1311
1312 return u'\n'.join([ data % p.fields_as_dict(date_format = '%Y %b %d', escape_style = self.__esc_style) for p in probs ])
1313
1316
1319
1320 - def _get_variant_text_snippet(self, data=None):
1321 data_parts = data.split(u'//')
1322 keyword = data_parts[0]
1323 template = u'%s'
1324 if len(data_parts) > 1:
1325 template = data_parts[1]
1326
1327 expansion = gmKeywordExpansionWidgets.expand_keyword(keyword = keyword, show_list = True)
1328
1329 if expansion is None:
1330 if self.debug:
1331 return self._escape(_('no textual expansion found for keyword <%s>') % keyword)
1332 return u''
1333
1334
1335 return template % expansion
1336
1392
1393 - def _get_variant_free_text(self, data=u'tex//'):
1394
1395
1396
1397
1398 data_parts = data.split('//')
1399 format = data_parts[0]
1400 if len(data_parts) > 1:
1401 msg = data_parts[1]
1402 else:
1403 msg = _('generic text')
1404
1405 dlg = gmGuiHelpers.cMultilineTextEntryDlg (
1406 None,
1407 -1,
1408 title = _('Replacing <free_text> placeholder'),
1409 msg = _('Below you can enter free text.\n\n [%s]') % msg
1410 )
1411 dlg.enable_user_formatting = True
1412 decision = dlg.ShowModal()
1413
1414 if decision != wx.ID_SAVE:
1415 dlg.Destroy()
1416 if self.debug:
1417 return self._escape(_('Text input cancelled by user.'))
1418 return u''
1419
1420 text = dlg.value.strip()
1421 if dlg.is_user_formatted:
1422 dlg.Destroy()
1423 return text
1424
1425 dlg.Destroy()
1426
1427
1428
1429
1430
1431 return self._escape(text)
1432
1446
1460
1461
1462
1464 if self.__esc_func is None:
1465 return text
1466 return self.__esc_func(text)
1467
1469 """Functions a macro can legally use.
1470
1471 An instance of this class is passed to the GNUmed scripting
1472 listener. Hence, all actions a macro can legally take must
1473 be defined in this class. Thus we achieve some screening for
1474 security and also thread safety handling.
1475 """
1476
1477 - def __init__(self, personality = None):
1478 if personality is None:
1479 raise gmExceptions.ConstructorError, 'must specify personality'
1480 self.__personality = personality
1481 self.__attached = 0
1482 self._get_source_personality = None
1483 self.__user_done = False
1484 self.__user_answer = 'no answer yet'
1485 self.__pat = gmPerson.gmCurrentPatient()
1486
1487 self.__auth_cookie = str(random.random())
1488 self.__pat_lock_cookie = str(random.random())
1489 self.__lock_after_load_cookie = str(random.random())
1490
1491 _log.info('slave mode personality is [%s]', personality)
1492
1493
1494
1495 - def attach(self, personality = None):
1496 if self.__attached:
1497 _log.error('attach with [%s] rejected, already serving a client', personality)
1498 return (0, _('attach rejected, already serving a client'))
1499 if personality != self.__personality:
1500 _log.error('rejecting attach to personality [%s], only servicing [%s]' % (personality, self.__personality))
1501 return (0, _('attach to personality [%s] rejected') % personality)
1502 self.__attached = 1
1503 self.__auth_cookie = str(random.random())
1504 return (1, self.__auth_cookie)
1505
1506 - def detach(self, auth_cookie=None):
1507 if not self.__attached:
1508 return 1
1509 if auth_cookie != self.__auth_cookie:
1510 _log.error('rejecting detach() with cookie [%s]' % auth_cookie)
1511 return 0
1512 self.__attached = 0
1513 return 1
1514
1516 if not self.__attached:
1517 return 1
1518 self.__user_done = False
1519
1520 wx.CallAfter(self._force_detach)
1521 return 1
1522
1524 ver = _cfg.get(option = u'client_version')
1525 return "GNUmed %s, %s $Revision: 1.51 $" % (ver, self.__class__.__name__)
1526
1528 """Shuts down this client instance."""
1529 if not self.__attached:
1530 return 0
1531 if auth_cookie != self.__auth_cookie:
1532 _log.error('non-authenticated shutdown_gnumed()')
1533 return 0
1534 wx.CallAfter(self._shutdown_gnumed, forced)
1535 return 1
1536
1538 """Raise ourselves to the top of the desktop."""
1539 if not self.__attached:
1540 return 0
1541 if auth_cookie != self.__auth_cookie:
1542 _log.error('non-authenticated raise_gnumed()')
1543 return 0
1544 return "cMacroPrimitives.raise_gnumed() not implemented"
1545
1547 if not self.__attached:
1548 return 0
1549 if auth_cookie != self.__auth_cookie:
1550 _log.error('non-authenticated get_loaded_plugins()')
1551 return 0
1552 gb = gmGuiBroker.GuiBroker()
1553 return gb['horstspace.notebook.gui'].keys()
1554
1556 """Raise a notebook plugin within GNUmed."""
1557 if not self.__attached:
1558 return 0
1559 if auth_cookie != self.__auth_cookie:
1560 _log.error('non-authenticated raise_notebook_plugin()')
1561 return 0
1562
1563 wx.CallAfter(gmPlugin.raise_notebook_plugin, a_plugin)
1564 return 1
1565
1567 """Load external patient, perhaps create it.
1568
1569 Callers must use get_user_answer() to get status information.
1570 It is unsafe to proceed without knowing the completion state as
1571 the controlled client may be waiting for user input from a
1572 patient selection list.
1573 """
1574 if not self.__attached:
1575 return (0, _('request rejected, you are not attach()ed'))
1576 if auth_cookie != self.__auth_cookie:
1577 _log.error('non-authenticated load_patient_from_external_source()')
1578 return (0, _('rejected load_patient_from_external_source(), not authenticated'))
1579 if self.__pat.locked:
1580 _log.error('patient is locked, cannot load from external source')
1581 return (0, _('current patient is locked'))
1582 self.__user_done = False
1583 wx.CallAfter(self._load_patient_from_external_source)
1584 self.__lock_after_load_cookie = str(random.random())
1585 return (1, self.__lock_after_load_cookie)
1586
1588 if not self.__attached:
1589 return (0, _('request rejected, you are not attach()ed'))
1590 if auth_cookie != self.__auth_cookie:
1591 _log.error('non-authenticated lock_load_patient()')
1592 return (0, _('rejected lock_load_patient(), not authenticated'))
1593
1594 if lock_after_load_cookie != self.__lock_after_load_cookie:
1595 _log.warning('patient lock-after-load request rejected due to wrong cookie [%s]' % lock_after_load_cookie)
1596 return (0, 'patient lock-after-load request rejected, wrong cookie provided')
1597 self.__pat.locked = True
1598 self.__pat_lock_cookie = str(random.random())
1599 return (1, self.__pat_lock_cookie)
1600
1602 if not self.__attached:
1603 return (0, _('request rejected, you are not attach()ed'))
1604 if auth_cookie != self.__auth_cookie:
1605 _log.error('non-authenticated lock_into_patient()')
1606 return (0, _('rejected lock_into_patient(), not authenticated'))
1607 if self.__pat.locked:
1608 _log.error('patient is already locked')
1609 return (0, _('already locked into a patient'))
1610 searcher = gmPersonSearch.cPatientSearcher_SQL()
1611 if type(search_params) == types.DictType:
1612 idents = searcher.get_identities(search_dict=search_params)
1613 raise StandardError("must use dto, not search_dict")
1614 else:
1615 idents = searcher.get_identities(search_term=search_params)
1616 if idents is None:
1617 return (0, _('error searching for patient with [%s]/%s') % (search_term, search_dict))
1618 if len(idents) == 0:
1619 return (0, _('no patient found for [%s]/%s') % (search_term, search_dict))
1620
1621 if len(idents) > 1:
1622 return (0, _('several matching patients found for [%s]/%s') % (search_term, search_dict))
1623 if not gmPatSearchWidgets.set_active_patient(patient = idents[0]):
1624 return (0, _('cannot activate patient [%s] (%s/%s)') % (str(idents[0]), search_term, search_dict))
1625 self.__pat.locked = True
1626 self.__pat_lock_cookie = str(random.random())
1627 return (1, self.__pat_lock_cookie)
1628
1630 if not self.__attached:
1631 return (0, _('request rejected, you are not attach()ed'))
1632 if auth_cookie != self.__auth_cookie:
1633 _log.error('non-authenticated unlock_patient()')
1634 return (0, _('rejected unlock_patient, not authenticated'))
1635
1636 if not self.__pat.locked:
1637 return (1, '')
1638
1639 if unlock_cookie != self.__pat_lock_cookie:
1640 _log.warning('patient unlock request rejected due to wrong cookie [%s]' % unlock_cookie)
1641 return (0, 'patient unlock request rejected, wrong cookie provided')
1642 self.__pat.locked = False
1643 return (1, '')
1644
1646 if not self.__attached:
1647 return 0
1648 if auth_cookie != self.__auth_cookie:
1649 _log.error('non-authenticated select_identity()')
1650 return 0
1651 return "cMacroPrimitives.assume_staff_identity() not implemented"
1652
1654 if not self.__user_done:
1655 return (0, 'still waiting')
1656 self.__user_done = False
1657 return (1, self.__user_answer)
1658
1659
1660
1662 msg = _(
1663 'Someone tries to forcibly break the existing\n'
1664 'controlling connection. This may or may not\n'
1665 'have legitimate reasons.\n\n'
1666 'Do you want to allow breaking the connection ?'
1667 )
1668 can_break_conn = gmGuiHelpers.gm_show_question (
1669 aMessage = msg,
1670 aTitle = _('forced detach attempt')
1671 )
1672 if can_break_conn:
1673 self.__user_answer = 1
1674 else:
1675 self.__user_answer = 0
1676 self.__user_done = True
1677 if can_break_conn:
1678 self.__pat.locked = False
1679 self.__attached = 0
1680 return 1
1681
1683 top_win = wx.GetApp().GetTopWindow()
1684 if forced:
1685 top_win.Destroy()
1686 else:
1687 top_win.Close()
1688
1697
1698
1699
1700 if __name__ == '__main__':
1701
1702 if len(sys.argv) < 2:
1703 sys.exit()
1704
1705 if sys.argv[1] != 'test':
1706 sys.exit()
1707
1708 gmI18N.activate_locale()
1709 gmI18N.install_domain()
1710
1711
1713 handler = gmPlaceholderHandler()
1714 handler.debug = True
1715
1716 for placeholder in ['a', 'b']:
1717 print handler[placeholder]
1718
1719 pat = gmPersonSearch.ask_for_patient()
1720 if pat is None:
1721 return
1722
1723 gmPatSearchWidgets.set_active_patient(patient = pat)
1724
1725 print 'DOB (YYYY-MM-DD):', handler['date_of_birth::%Y-%m-%d']
1726
1727 app = wx.PyWidgetTester(size = (200, 50))
1728 for placeholder in known_placeholders:
1729 print placeholder, "=", handler[placeholder]
1730
1731 ph = 'progress_notes::ap'
1732 print '%s: %s' % (ph, handler[ph])
1733
1735
1736 tests = [
1737
1738 '$<lastname>$',
1739 '$<lastname::::3>$',
1740 '$<name::%(title)s %(firstnames)s%(preferred)s%(lastnames)s>$',
1741
1742
1743 'lastname',
1744 '$<lastname',
1745 '$<lastname::',
1746 '$<lastname::>$',
1747 '$<lastname::abc>$',
1748 '$<lastname::abc::>$',
1749 '$<lastname::abc::3>$',
1750 '$<lastname::abc::xyz>$',
1751 '$<lastname::::>$',
1752 '$<lastname::::xyz>$',
1753
1754 '$<date_of_birth::%Y-%m-%d>$',
1755 '$<date_of_birth::%Y-%m-%d::3>$',
1756 '$<date_of_birth::%Y-%m-%d::>$',
1757
1758
1759 '$<adr_location::home::35>$',
1760 '$<gender_mapper::male//female//other::5>$',
1761 '$<current_meds::==> %(brand)s %(preparation)s (%(substance)s) <==\n::50>$',
1762 '$<allergy_list::%(descriptor)s, >$',
1763 '$<current_meds_table::latex//by-brand>$'
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778 ]
1779
1780
1781
1782
1783
1784 pat = gmPersonSearch.ask_for_patient()
1785 if pat is None:
1786 return
1787
1788 gmPatSearchWidgets.set_active_patient(patient = pat)
1789
1790 handler = gmPlaceholderHandler()
1791 handler.debug = True
1792
1793 for placeholder in tests:
1794 print placeholder, "=>", handler[placeholder]
1795 print "--------------"
1796 raw_input()
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1809 from Gnumed.pycommon import gmScriptingListener
1810 import xmlrpclib
1811 listener = gmScriptingListener.cScriptingListener(macro_executor = cMacroPrimitives(personality='unit test'), port=9999)
1812
1813 s = xmlrpclib.ServerProxy('http://localhost:9999')
1814 print "should fail:", s.attach()
1815 print "should fail:", s.attach('wrong cookie')
1816 print "should work:", s.version()
1817 print "should fail:", s.raise_gnumed()
1818 print "should fail:", s.raise_notebook_plugin('test plugin')
1819 print "should fail:", s.lock_into_patient('kirk, james')
1820 print "should fail:", s.unlock_patient()
1821 status, conn_auth = s.attach('unit test')
1822 print "should work:", status, conn_auth
1823 print "should work:", s.version()
1824 print "should work:", s.raise_gnumed(conn_auth)
1825 status, pat_auth = s.lock_into_patient(conn_auth, 'kirk, james')
1826 print "should work:", status, pat_auth
1827 print "should fail:", s.unlock_patient(conn_auth, 'bogus patient unlock cookie')
1828 print "should work", s.unlock_patient(conn_auth, pat_auth)
1829 data = {'firstname': 'jame', 'lastnames': 'Kirk', 'gender': 'm'}
1830 status, pat_auth = s.lock_into_patient(conn_auth, data)
1831 print "should work:", status, pat_auth
1832 print "should work", s.unlock_patient(conn_auth, pat_auth)
1833 print s.detach('bogus detach cookie')
1834 print s.detach(conn_auth)
1835 del s
1836
1837 listener.shutdown()
1838
1840
1841 import re as regex
1842
1843 tests = [
1844 ' $<lastname>$ ',
1845 ' $<lastname::::3>$ ',
1846
1847
1848 '$<date_of_birth::%Y-%m-%d>$',
1849 '$<date_of_birth::%Y-%m-%d::3>$',
1850 '$<date_of_birth::%Y-%m-%d::>$',
1851
1852 '$<adr_location::home::35>$',
1853 '$<gender_mapper::male//female//other::5>$',
1854 '$<current_meds::==> %(brand)s %(preparation)s (%(substance)s) <==\\n::50>$',
1855 '$<allergy_list::%(descriptor)s, >$',
1856
1857 '\\noindent Patient: $<lastname>$, $<firstname>$',
1858 '$<allergies::%(descriptor)s & %(l10n_type)s & {\\footnotesize %(reaction)s} \tabularnewline \hline >$',
1859 '$<current_meds:: \item[%(substance)s] {\\footnotesize (%(brand)s)} %(preparation)s %(amount)s%(unit)s: %(schedule)s >$'
1860 ]
1861
1862 tests = [
1863
1864 'junk $<lastname::::3>$ junk',
1865 'junk $<lastname::abc::3>$ junk',
1866 'junk $<lastname::abc>$ junk',
1867 'junk $<lastname>$ junk',
1868
1869 'junk $<lastname>$ junk $<firstname>$ junk',
1870 'junk $<lastname::abc>$ junk $<fiststname::abc>$ junk',
1871 'junk $<lastname::abc::3>$ junk $<firstname::abc::3>$ junk',
1872 'junk $<lastname::::3>$ junk $<firstname::::3>$ junk'
1873
1874 ]
1875
1876 print "testing placeholder regex:", default_placeholder_regex
1877 print ""
1878
1879 for t in tests:
1880 print 'line: "%s"' % t
1881 print "placeholders:"
1882 for p in regex.findall(default_placeholder_regex, t, regex.IGNORECASE):
1883 print ' => "%s"' % p
1884 print " "
1885
1887
1888 phs = [
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926 u'$<test_results::%(unified_abbrev)s: %(unified_val)s %(val_unit)s//%c::>$'
1927 ]
1928
1929 handler = gmPlaceholderHandler()
1930 handler.debug = True
1931
1932 gmStaff.set_current_provider_to_logged_on_user()
1933 pat = gmPersonSearch.ask_for_patient()
1934 if pat is None:
1935 return
1936
1937 gmPatSearchWidgets.set_active_patient(patient = pat)
1938
1939 app = wx.PyWidgetTester(size = (200, 50))
1940
1941 for ph in phs:
1942 print ph
1943 print "result:"
1944 print '%s' % handler[ph]
1945
1946
1954
1957
1958
1959
1960
1961
1962
1963
1964 test_placeholder()
1965
1966
1967
1968