1 """GNUmed macro primitives.
2
3 This module implements functions a macro can legally use.
4 """
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, gmGuiBroker, gmExceptions, gmBorg, gmTools
19 from Gnumed.pycommon import gmCfg2, gmDateTime
20 from Gnumed.business import gmPerson, gmDemographicRecord
21 from Gnumed.wxpython import gmGuiHelpers, gmPlugin, gmPatSearchWidgets, gmNarrativeWidgets
22
23
24 _log = logging.getLogger('gm.scripting')
25 _cfg = gmCfg2.gmCfgData()
26
27
28 known_placeholders = [
29 'lastname',
30 'firstname',
31 'title',
32 'date_of_birth',
33 'progress_notes',
34 'soap',
35 'soap_s',
36 'soap_o',
37 'soap_a',
38 'soap_p',
39 u'client_version',
40 u'current_provider',
41 u'allergy_state'
42 ]
43
44
45
46 known_variant_placeholders = [
47 u'soap',
48 u'progress_notes',
49 u'date_of_birth',
50 u'adr_street',
51 u'adr_number',
52 u'adr_location',
53 u'adr_postcode',
54 u'gender_mapper',
55 u'current_meds',
56 u'today',
57 u'tex_escape',
58 u'allergies',
59 u'allergy_list',
60 u'problems',
61 u'name'
62 ]
63
64 default_placeholder_regex = r'\$<.+?>\$'
65
66
67
68
69
70
71
72
73 default_placeholder_start = u'$<'
74 default_placeholder_end = u'>$'
75
77 """Replaces placeholders in forms, fields, etc.
78
79 - patient related placeholders operate on the currently active patient
80 - is passed to the forms handling code, for example
81
82 Note that this cannot be called from a non-gui thread unless
83 wrapped in wx.CallAfter.
84
85 There are currently three types of placeholders:
86
87 simple static placeholders
88 - those are listed in known_placeholders
89 - they are used as-is
90
91 extended static placeholders
92 - those are like the static ones but have "::::<NUMBER>" appended
93 where <NUMBER> is the maximum length
94
95 variant placeholders
96 - those are listed in known_variant_placeholders
97 - they are parsed into placeholder, data, and maximum length
98 - the length is optional
99 - data is passed to the handler
100 """
102
103 self.pat = gmPerson.gmCurrentPatient()
104 self.debug = False
105
106 self.invalid_placeholder_template = _('invalid placeholder [%s]')
107
108
109
111 """Map self['placeholder'] to self.placeholder.
112
113 This is useful for replacing placeholders parsed out
114 of documents as strings.
115
116 Unknown/invalid placeholders still deliver a result but
117 it will be glaringly obvious if debugging is enabled.
118 """
119 _log.debug('replacing [%s]', placeholder)
120
121 original_placeholder = placeholder
122
123 if placeholder.startswith(default_placeholder_start):
124 placeholder = placeholder[len(default_placeholder_start):]
125 if placeholder.endswith(default_placeholder_end):
126 placeholder = placeholder[:-len(default_placeholder_end)]
127 else:
128 _log.debug('placeholder must either start with [%s] and end with [%s] or neither of both', default_placeholder_start, default_placeholder_end)
129 if self.debug:
130 return self.invalid_placeholder_template % original_placeholder
131 return None
132
133
134 if placeholder in known_placeholders:
135 return getattr(self, placeholder)
136
137
138 parts = placeholder.split('::::', 1)
139 if len(parts) == 2:
140 name, lng = parts
141 try:
142 return getattr(self, name)[:int(lng)]
143 except:
144 _log.exception('placeholder handling error: %s', original_placeholder)
145 if self.debug:
146 return self.invalid_placeholder_template % original_placeholder
147 return None
148
149
150 parts = placeholder.split('::', 2)
151 if len(parts) == 2:
152 name, data = parts
153 lng = None
154 elif len(parts) == 3:
155 name, data, lng = parts
156 try:
157 lng = int(lng)
158 except:
159 _log.exception('placeholder length definition error: %s, discarding length', original_placeholder)
160 lng = None
161 else:
162 _log.warning('invalid placeholder layout: %s', original_placeholder)
163 if self.debug:
164 return self.invalid_placeholder_template % original_placeholder
165 return None
166
167 handler = getattr(self, '_get_variant_%s' % name, None)
168 if handler is None:
169 _log.warning('no handler <_get_variant_%s> for placeholder %s', name, original_placeholder)
170 if self.debug:
171 return self.invalid_placeholder_template % original_placeholder
172 return None
173
174 try:
175 if lng is None:
176 return handler(data = data)
177 return handler(data = data)[:lng]
178 except:
179 _log.exception('placeholder handling error: %s', original_placeholder)
180 if self.debug:
181 return self.invalid_placeholder_template % original_placeholder
182 return None
183
184 _log.error('something went wrong, should never get here')
185 return None
186
187
188
189
190
192 """This does nothing, used as a NOOP properties setter."""
193 pass
194
197
200
203
205 return self._get_variant_date_of_birth(data='%x')
206
208 return self._get_variant_soap()
209
211 return self._get_variant_soap(data = u's')
212
214 return self._get_variant_soap(data = u'o')
215
217 return self._get_variant_soap(data = u'a')
218
220 return self._get_variant_soap(data = u'p')
221
223 return self._get_variant_soap(soap_cats = None)
224
226 return gmTools.coalesce (
227 _cfg.get(option = u'client_version'),
228 u'%s' % self.__class__.__name__
229 )
230
246
254
255
256
257 placeholder_regex = property(lambda x: default_placeholder_regex, _setter_noop)
258
259
260 lastname = property(_get_lastname, _setter_noop)
261 firstname = property(_get_firstname, _setter_noop)
262 title = property(_get_title, _setter_noop)
263 date_of_birth = property(_get_dob, _setter_noop)
264
265 progress_notes = property(_get_progress_notes, _setter_noop)
266 soap = property(_get_progress_notes, _setter_noop)
267 soap_s = property(_get_soap_s, _setter_noop)
268 soap_o = property(_get_soap_o, _setter_noop)
269 soap_a = property(_get_soap_a, _setter_noop)
270 soap_p = property(_get_soap_p, _setter_noop)
271 soap_admin = property(_get_soap_admin, _setter_noop)
272
273 allergy_state = property(_get_allergy_state, _setter_noop)
274
275 client_version = property(_get_client_version, _setter_noop)
276
277 current_provider = property(_get_current_provider, _setter_noop)
278
279
280
282 return self._get_variant_soap(data=data)
283
285 if data is None:
286 cats = list(data)
287 template = u'%s'
288 else:
289 parts = data.split('//', 2)
290 if len(parts) == 1:
291 cats = list(parts)
292 template = u'%s'
293 else:
294 cats = list(parts[0])
295 template = parts[1]
296
297 narr = gmNarrativeWidgets.select_narrative_from_episodes(soap_cats = cats)
298
299 if len(narr) == 0:
300 return u''
301
302 narr = [ template % n['narrative'] for n in narr ]
303
304 return u'\n'.join(narr)
305
307 if data is None:
308 return [_('template is missing')]
309
310 name = self.pat.get_active_name()
311
312 parts = {
313 'title': gmTools.coalesce(name['title'], u''),
314 'firstnames': name['firstnames'],
315 'lastnames': name['lastnames'],
316 'preferred': gmTools.coalesce (
317 initial = name['preferred'],
318 instead = u' ',
319 template_initial = u' "%s" '
320 )
321 }
322
323 return data % parts
324
327
328
330 values = data.split('//', 2)
331
332 if len(values) == 2:
333 male_value, female_value = values
334 other_value = u'<unkown gender>'
335 elif len(values) == 3:
336 male_value, female_value, other_value = values
337 else:
338 return _('invalid gender mapping layout: [%s]') % data
339
340 if self.pat['gender'] == u'm':
341 return male_value
342
343 if self.pat['gender'] == u'f':
344 return female_value
345
346 return other_value
347
349
350
351 adrs = self.pat.get_addresses(address_type=data)
352 if len(adrs) == 0:
353 return _('no street for address type [%s]') % data
354 return adrs[0]['street']
355
357 adrs = self.pat.get_addresses(address_type=data)
358 if len(adrs) == 0:
359 return _('no number for address type [%s]') % data
360 return adrs[0]['number']
361
363 adrs = self.pat.get_addresses(address_type=data)
364 if len(adrs) == 0:
365 return _('no location for address type [%s]') % data
366 return adrs[0]['urb']
367
368 - def _get_variant_adr_postcode(self, data=u'?'):
369 adrs = self.pat.get_addresses(address_type=data)
370 if len(adrs) == 0:
371 return _('no postcode for address type [%s]') % data
372 return adrs[0]['postcode']
373
375 if data is None:
376 return [_('template is missing')]
377
378 template, separator = data.split('//', 2)
379
380 emr = self.pat.get_emr()
381 return separator.join([ template % a for a in emr.get_allergies() ])
382
384
385 if data is None:
386 return [_('template is missing')]
387
388 emr = self.pat.get_emr()
389 return u'\n'.join([ data % a for a in emr.get_allergies() ])
390
392
393 if data is None:
394 return [_('template is missing')]
395
396 emr = self.pat.get_emr()
397 current_meds = emr.get_current_substance_intake (
398 include_inactive = False,
399 include_unapproved = False,
400 order_by = u'brand, substance'
401 )
402
403 return u'\n'.join([ data % m for m in current_meds ])
404
406
407 if data is None:
408 return [_('template is missing')]
409
410 probs = self.pat.get_emr().get_problems()
411
412 return u'\n'.join([ data % p for p in probs ])
413
416
419
420
421
422
423
425 """Functions a macro can legally use.
426
427 An instance of this class is passed to the GNUmed scripting
428 listener. Hence, all actions a macro can legally take must
429 be defined in this class. Thus we achieve some screening for
430 security and also thread safety handling.
431 """
432
433 - def __init__(self, personality = None):
434 if personality is None:
435 raise gmExceptions.ConstructorError, 'must specify personality'
436 self.__personality = personality
437 self.__attached = 0
438 self._get_source_personality = None
439 self.__user_done = False
440 self.__user_answer = 'no answer yet'
441 self.__pat = gmPerson.gmCurrentPatient()
442
443 self.__auth_cookie = str(random.random())
444 self.__pat_lock_cookie = str(random.random())
445 self.__lock_after_load_cookie = str(random.random())
446
447 _log.info('slave mode personality is [%s]', personality)
448
449
450
451 - def attach(self, personality = None):
452 if self.__attached:
453 _log.error('attach with [%s] rejected, already serving a client', personality)
454 return (0, _('attach rejected, already serving a client'))
455 if personality != self.__personality:
456 _log.error('rejecting attach to personality [%s], only servicing [%s]' % (personality, self.__personality))
457 return (0, _('attach to personality [%s] rejected') % personality)
458 self.__attached = 1
459 self.__auth_cookie = str(random.random())
460 return (1, self.__auth_cookie)
461
462 - def detach(self, auth_cookie=None):
463 if not self.__attached:
464 return 1
465 if auth_cookie != self.__auth_cookie:
466 _log.error('rejecting detach() with cookie [%s]' % auth_cookie)
467 return 0
468 self.__attached = 0
469 return 1
470
472 if not self.__attached:
473 return 1
474 self.__user_done = False
475
476 wx.CallAfter(self._force_detach)
477 return 1
478
480 ver = _cfg.get(option = u'client_version')
481 return "GNUmed %s, %s $Revision: 1.51 $" % (ver, self.__class__.__name__)
482
484 """Shuts down this client instance."""
485 if not self.__attached:
486 return 0
487 if auth_cookie != self.__auth_cookie:
488 _log.error('non-authenticated shutdown_gnumed()')
489 return 0
490 wx.CallAfter(self._shutdown_gnumed, forced)
491 return 1
492
494 """Raise ourselves to the top of the desktop."""
495 if not self.__attached:
496 return 0
497 if auth_cookie != self.__auth_cookie:
498 _log.error('non-authenticated raise_gnumed()')
499 return 0
500 return "cMacroPrimitives.raise_gnumed() not implemented"
501
503 if not self.__attached:
504 return 0
505 if auth_cookie != self.__auth_cookie:
506 _log.error('non-authenticated get_loaded_plugins()')
507 return 0
508 gb = gmGuiBroker.GuiBroker()
509 return gb['horstspace.notebook.gui'].keys()
510
512 """Raise a notebook plugin within GNUmed."""
513 if not self.__attached:
514 return 0
515 if auth_cookie != self.__auth_cookie:
516 _log.error('non-authenticated raise_notebook_plugin()')
517 return 0
518
519 wx.CallAfter(gmPlugin.raise_notebook_plugin, a_plugin)
520 return 1
521
523 """Load external patient, perhaps create it.
524
525 Callers must use get_user_answer() to get status information.
526 It is unsafe to proceed without knowing the completion state as
527 the controlled client may be waiting for user input from a
528 patient selection list.
529 """
530 if not self.__attached:
531 return (0, _('request rejected, you are not attach()ed'))
532 if auth_cookie != self.__auth_cookie:
533 _log.error('non-authenticated load_patient_from_external_source()')
534 return (0, _('rejected load_patient_from_external_source(), not authenticated'))
535 if self.__pat.locked:
536 _log.error('patient is locked, cannot load from external source')
537 return (0, _('current patient is locked'))
538 self.__user_done = False
539 wx.CallAfter(self._load_patient_from_external_source)
540 self.__lock_after_load_cookie = str(random.random())
541 return (1, self.__lock_after_load_cookie)
542
544 if not self.__attached:
545 return (0, _('request rejected, you are not attach()ed'))
546 if auth_cookie != self.__auth_cookie:
547 _log.error('non-authenticated lock_load_patient()')
548 return (0, _('rejected lock_load_patient(), not authenticated'))
549
550 if lock_after_load_cookie != self.__lock_after_load_cookie:
551 _log.warning('patient lock-after-load request rejected due to wrong cookie [%s]' % lock_after_load_cookie)
552 return (0, 'patient lock-after-load request rejected, wrong cookie provided')
553 self.__pat.locked = True
554 self.__pat_lock_cookie = str(random.random())
555 return (1, self.__pat_lock_cookie)
556
558 if not self.__attached:
559 return (0, _('request rejected, you are not attach()ed'))
560 if auth_cookie != self.__auth_cookie:
561 _log.error('non-authenticated lock_into_patient()')
562 return (0, _('rejected lock_into_patient(), not authenticated'))
563 if self.__pat.locked:
564 _log.error('patient is already locked')
565 return (0, _('already locked into a patient'))
566 searcher = gmPerson.cPatientSearcher_SQL()
567 if type(search_params) == types.DictType:
568 idents = searcher.get_identities(search_dict=search_params)
569 print "must use dto, not search_dict"
570 print xxxxxxxxxxxxxxxxx
571 else:
572 idents = searcher.get_identities(search_term=search_params)
573 if idents is None:
574 return (0, _('error searching for patient with [%s]/%s') % (search_term, search_dict))
575 if len(idents) == 0:
576 return (0, _('no patient found for [%s]/%s') % (search_term, search_dict))
577
578 if len(idents) > 1:
579 return (0, _('several matching patients found for [%s]/%s') % (search_term, search_dict))
580 if not gmPatSearchWidgets.set_active_patient(patient = idents[0]):
581 return (0, _('cannot activate patient [%s] (%s/%s)') % (str(idents[0]), search_term, search_dict))
582 self.__pat.locked = True
583 self.__pat_lock_cookie = str(random.random())
584 return (1, self.__pat_lock_cookie)
585
587 if not self.__attached:
588 return (0, _('request rejected, you are not attach()ed'))
589 if auth_cookie != self.__auth_cookie:
590 _log.error('non-authenticated unlock_patient()')
591 return (0, _('rejected unlock_patient, not authenticated'))
592
593 if not self.__pat.locked:
594 return (1, '')
595
596 if unlock_cookie != self.__pat_lock_cookie:
597 _log.warning('patient unlock request rejected due to wrong cookie [%s]' % unlock_cookie)
598 return (0, 'patient unlock request rejected, wrong cookie provided')
599 self.__pat.locked = False
600 return (1, '')
601
603 if not self.__attached:
604 return 0
605 if auth_cookie != self.__auth_cookie:
606 _log.error('non-authenticated select_identity()')
607 return 0
608 return "cMacroPrimitives.assume_staff_identity() not implemented"
609
611 if not self.__user_done:
612 return (0, 'still waiting')
613 self.__user_done = False
614 return (1, self.__user_answer)
615
616
617
619 msg = _(
620 'Someone tries to forcibly break the existing\n'
621 'controlling connection. This may or may not\n'
622 'have legitimate reasons.\n\n'
623 'Do you want to allow breaking the connection ?'
624 )
625 can_break_conn = gmGuiHelpers.gm_show_question (
626 aMessage = msg,
627 aTitle = _('forced detach attempt')
628 )
629 if can_break_conn:
630 self.__user_answer = 1
631 else:
632 self.__user_answer = 0
633 self.__user_done = True
634 if can_break_conn:
635 self.__pat.locked = False
636 self.__attached = 0
637 return 1
638
640 top_win = wx.GetApp().GetTopWindow()
641 if forced:
642 top_win.Destroy()
643 else:
644 top_win.Close()
645
654
655
656
657 if __name__ == '__main__':
658
659 gmI18N.activate_locale()
660 gmI18N.install_domain()
661
662
664 handler = gmPlaceholderHandler()
665 handler.debug = True
666
667 for placeholder in ['a', 'b']:
668 print handler[placeholder]
669
670 pat = gmPerson.ask_for_patient()
671 if pat is None:
672 return
673
674 gmPatSearchWidgets.set_active_patient(patient = pat)
675
676 print 'DOB (YYYY-MM-DD):', handler['date_of_birth::%Y-%m-%d']
677
678 app = wx.PyWidgetTester(size = (200, 50))
679 for placeholder in known_placeholders:
680 print placeholder, "=", handler[placeholder]
681
682 ph = 'progress_notes::ap'
683 print '%s: %s' % (ph, handler[ph])
684
686
687 tests = [
688
689 '$<lastname>$',
690 '$<lastname::::3>$',
691 '$<name::%(title)s %(firstnames)s%(preferred)s%(lastnames)s>$',
692
693
694 'lastname',
695 '$<lastname',
696 '$<lastname::',
697 '$<lastname::>$',
698 '$<lastname::abc>$',
699 '$<lastname::abc::>$',
700 '$<lastname::abc::3>$',
701 '$<lastname::abc::xyz>$',
702 '$<lastname::::>$',
703 '$<lastname::::xyz>$',
704
705 '$<date_of_birth::%Y-%m-%d>$',
706 '$<date_of_birth::%Y-%m-%d::3>$',
707 '$<date_of_birth::%Y-%m-%d::>$',
708
709
710 '$<adr_location::home::35>$',
711 '$<gender_mapper::male//female//other::5>$',
712 '$<current_meds::==> %(brand)s %(preparation)s (%(substance)s) <==\n::50>$',
713 '$<allergy_list::%(descriptor)s, >$'
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728 ]
729
730 pat = gmPerson.ask_for_patient()
731 if pat is None:
732 return
733
734 gmPatSearchWidgets.set_active_patient(patient = pat)
735
736 handler = gmPlaceholderHandler()
737 handler.debug = True
738
739 for placeholder in tests:
740 print placeholder, "=>", handler[placeholder]
741 print "--------------"
742 raw_input()
743
744
745
746
747
748
749
750
751
752
753
755 from Gnumed.pycommon import gmScriptingListener
756 import xmlrpclib
757 listener = gmScriptingListener.cScriptingListener(macro_executor = cMacroPrimitives(personality='unit test'), port=9999)
758
759 s = xmlrpclib.ServerProxy('http://localhost:9999')
760 print "should fail:", s.attach()
761 print "should fail:", s.attach('wrong cookie')
762 print "should work:", s.version()
763 print "should fail:", s.raise_gnumed()
764 print "should fail:", s.raise_notebook_plugin('test plugin')
765 print "should fail:", s.lock_into_patient('kirk, james')
766 print "should fail:", s.unlock_patient()
767 status, conn_auth = s.attach('unit test')
768 print "should work:", status, conn_auth
769 print "should work:", s.version()
770 print "should work:", s.raise_gnumed(conn_auth)
771 status, pat_auth = s.lock_into_patient(conn_auth, 'kirk, james')
772 print "should work:", status, pat_auth
773 print "should fail:", s.unlock_patient(conn_auth, 'bogus patient unlock cookie')
774 print "should work", s.unlock_patient(conn_auth, pat_auth)
775 data = {'firstname': 'jame', 'lastnames': 'Kirk', 'gender': 'm'}
776 status, pat_auth = s.lock_into_patient(conn_auth, data)
777 print "should work:", status, pat_auth
778 print "should work", s.unlock_patient(conn_auth, pat_auth)
779 print s.detach('bogus detach cookie')
780 print s.detach(conn_auth)
781 del s
782
783 listener.shutdown()
784
786
787 import re as regex
788
789 tests = [
790 ' $<lastname>$ ',
791 ' $<lastname::::3>$ ',
792
793
794 '$<date_of_birth::%Y-%m-%d>$',
795 '$<date_of_birth::%Y-%m-%d::3>$',
796 '$<date_of_birth::%Y-%m-%d::>$',
797
798 '$<adr_location::home::35>$',
799 '$<gender_mapper::male//female//other::5>$',
800 '$<current_meds::==> %(brand)s %(preparation)s (%(substance)s) <==\\n::50>$',
801 '$<allergy_list::%(descriptor)s, >$',
802
803 '\\noindent Patient: $<lastname>$, $<firstname>$',
804 '$<allergies::%(descriptor)s & %(l10n_type)s & {\\footnotesize %(reaction)s} \tabularnewline \hline >$',
805 '$<current_meds:: \item[%(substance)s] {\\footnotesize (%(brand)s)} %(preparation)s %(strength)s: %(schedule)s >$'
806 ]
807
808 tests = [
809
810 'junk $<lastname::::3>$ junk',
811 'junk $<lastname::abc::3>$ junk',
812 'junk $<lastname::abc>$ junk',
813 'junk $<lastname>$ junk',
814
815 'junk $<lastname>$ junk $<firstname>$ junk',
816 'junk $<lastname::abc>$ junk $<fiststname::abc>$ junk',
817 'junk $<lastname::abc::3>$ junk $<firstname::abc::3>$ junk',
818 'junk $<lastname::::3>$ junk $<firstname::::3>$ junk'
819
820 ]
821
822 print "testing placeholder regex:", default_placeholder_regex
823 print ""
824
825 for t in tests:
826 print 'line: "%s"' % t
827 print "placeholders:"
828 for p in regex.findall(default_placeholder_regex, t, regex.IGNORECASE):
829 print ' => "%s"' % p
830 print " "
831
832
833 if len(sys.argv) > 1 and sys.argv[1] == 'test':
834
835 test_new_variant_placeholders()
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019