1 """GNUmed macro primitives.
2
3 This module implements functions a macro can legally use.
4 """
5
6
7 __version__ = "$Revision: 1.50 $"
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 ]
62
63
64 default_placeholder_regex = r'\$<.+?>\$'
65 default_placeholder_start = u'$<'
66 default_placeholder_end = u'>$'
67
69 """Replaces placeholders in forms, fields, etc.
70
71 - patient related placeholders operate on the currently active patient
72 - is passed to the forms handling code, for example
73
74 Note that this cannot be called from a non-gui thread unless
75 wrapped in wx.CallAfter.
76
77 There are currently three types of placeholders:
78
79 simple static placeholders
80 - those are listed in known_placeholders
81 - they are used as-is
82
83 extended static placeholders
84 - those are like the static ones but have "::::<NUMBER>" appended
85 where <NUMBER> is the maximum length
86
87 variant placeholders
88 - those are listed in known_variant_placeholders
89 - they are parsed into placeholder, data, and maximum length
90 - the length is optional
91 - data is passed to the handler
92 """
94
95 self.pat = gmPerson.gmCurrentPatient()
96 self.debug = False
97
98 self.invalid_placeholder_template = _('invalid placeholder [%s]')
99
100
101
103 """Map self['placeholder'] to self.placeholder.
104
105 This is useful for replacing placeholders parsed out
106 of documents as strings.
107
108 Unknown/invalid placeholders still deliver a result but
109 it will be glaringly obvious if debugging is enabled.
110 """
111 _log.debug('replacing [%s]', placeholder)
112
113 original_placeholder = placeholder
114
115 if placeholder.startswith(default_placeholder_start):
116 placeholder = placeholder[len(default_placeholder_start):]
117 if placeholder.endswith(default_placeholder_end):
118 placeholder = placeholder[:-len(default_placeholder_end)]
119 else:
120 _log.debug('placeholder must either start with [%s] and end with [%s] or neither of both', default_placeholder_start, default_placeholder_end)
121 if self.debug:
122 return self.invalid_placeholder_template % original_placeholder
123 return None
124
125
126 if placeholder in known_placeholders:
127 return getattr(self, placeholder)
128
129
130 parts = placeholder.split('::::', 1)
131 if len(parts) == 2:
132 name, lng = parts
133 try:
134 return getattr(self, name)[:int(lng)]
135 except:
136 _log.exception('placeholder handling error: %s', original_placeholder)
137 if self.debug:
138 return self.invalid_placeholder_template % original_placeholder
139 return None
140
141
142 parts = placeholder.split('::', 2)
143 if len(parts) == 2:
144 name, data = parts
145 lng = None
146 elif len(parts) == 3:
147 name, data, lng = parts
148 try:
149 lng = int(lng)
150 except:
151 _log.exception('placeholder length definition error: %s, discarding length', original_placeholder)
152 lng = None
153 else:
154 _log.warning('invalid placeholder layout: %s', original_placeholder)
155 if self.debug:
156 return self.invalid_placeholder_template % original_placeholder
157 return None
158
159 handler = getattr(self, '_get_variant_%s' % name, None)
160 if handler is None:
161 _log.warning('no handler <_get_variant_%s> for placeholder %s', name, original_placeholder)
162 if self.debug:
163 return self.invalid_placeholder_template % original_placeholder
164 return None
165
166 try:
167 if lng is None:
168 return handler(data = data)
169 return handler(data = data)[:lng]
170 except:
171 _log.exception('placeholder handling error: %s', original_placeholder)
172 if self.debug:
173 return self.invalid_placeholder_template % original_placeholder
174 return None
175
176 _log.error('something went wrong, should never get here')
177 return None
178
179
180
181
182
184 """This does nothing, used as a NOOP properties setter."""
185 pass
186
189
192
195
197 return self._get_variant_date_of_birth(data='%x')
198
200 return self._get_variant_soap()
201
203 return self._get_variant_soap(data = u's')
204
206 return self._get_variant_soap(data = u'o')
207
209 return self._get_variant_soap(data = u'a')
210
212 return self._get_variant_soap(data = u'p')
213
215 return self._get_variant_soap(soap_cats = None)
216
218 return gmTools.coalesce (
219 _cfg.get(option = u'client_version'),
220 u'%s' % self.__class__.__name__
221 )
222
238
246
247
248
249 placeholder_regex = property(lambda x: default_placeholder_regex, _setter_noop)
250
251
252 lastname = property(_get_lastname, _setter_noop)
253 firstname = property(_get_firstname, _setter_noop)
254 title = property(_get_title, _setter_noop)
255 date_of_birth = property(_get_dob, _setter_noop)
256
257 progress_notes = property(_get_progress_notes, _setter_noop)
258 soap = property(_get_progress_notes, _setter_noop)
259 soap_s = property(_get_soap_s, _setter_noop)
260 soap_o = property(_get_soap_o, _setter_noop)
261 soap_a = property(_get_soap_a, _setter_noop)
262 soap_p = property(_get_soap_p, _setter_noop)
263 soap_admin = property(_get_soap_admin, _setter_noop)
264
265 allergy_state = property(_get_allergy_state, _setter_noop)
266
267 client_version = property(_get_client_version, _setter_noop)
268
269 current_provider = property(_get_current_provider, _setter_noop)
270
271
272
274 return self._get_variant_soap(data=data)
275
277 if data is None:
278 cats = list(data)
279 template = u'%s'
280 else:
281 parts = data.split('//', 2)
282 if len(parts) == 1:
283 cats = list(parts)
284 template = u'%s'
285 else:
286 cats = list(parts[0])
287 template = parts[1]
288
289 narr = gmNarrativeWidgets.select_narrative_from_episodes(soap_cats = cats)
290
291 if len(narr) == 0:
292 return u''
293
294 narr = [ template % n['narrative'] for n in narr ]
295
296 return u'\n'.join(narr)
297
300
301
303 values = data.split('//', 2)
304
305 if len(values) == 2:
306 male_value, female_value = values
307 other_value = u'<unkown gender>'
308 elif len(values) == 3:
309 male_value, female_value, other_value = values
310 else:
311 return _('invalid gender mapping layout: [%s]') % data
312
313 if self.pat['gender'] == u'm':
314 return male_value
315
316 if self.pat['gender'] == u'f':
317 return female_value
318
319 return other_value
320
322
323
324 adrs = self.pat.get_addresses(address_type=data)
325 if len(adrs) == 0:
326 return _('no street for address type [%s]') % data
327 return adrs[0]['street']
328
330 adrs = self.pat.get_addresses(address_type=data)
331 if len(adrs) == 0:
332 return _('no number for address type [%s]') % data
333 return adrs[0]['number']
334
336 adrs = self.pat.get_addresses(address_type=data)
337 if len(adrs) == 0:
338 return _('no location for address type [%s]') % data
339 return adrs[0]['urb']
340
341 - def _get_variant_adr_postcode(self, data=u'?'):
342 adrs = self.pat.get_addresses(address_type=data)
343 if len(adrs) == 0:
344 return _('no postcode for address type [%s]') % data
345 return adrs[0]['postcode']
346
348 if data is None:
349 return [_('template is missing')]
350
351 template, separator = data.split('//', 2)
352
353 emr = self.pat.get_emr()
354 return separator.join([ template % a for a in emr.get_allergies() ])
355
357
358 if data is None:
359 return [_('template is missing')]
360
361 emr = self.pat.get_emr()
362 return u'\n'.join([ data % a for a in emr.get_allergies() ])
363
365
366 if data is None:
367 return [_('template is missing')]
368
369 emr = self.pat.get_emr()
370 current_meds = emr.get_current_substance_intake (
371 include_inactive = False,
372 include_unapproved = False,
373 order_by = u'brand, substance'
374 )
375
376 return u'\n'.join([ data % m for m in current_meds ])
377
379
380 if data is None:
381 return [_('template is missing')]
382
383 probs = self.pat.get_emr().get_problems()
384
385 return u'\n'.join([ data % p for p in probs ])
386
389
392
393
394
395
396
398 """Functions a macro can legally use.
399
400 An instance of this class is passed to the GNUmed scripting
401 listener. Hence, all actions a macro can legally take must
402 be defined in this class. Thus we achieve some screening for
403 security and also thread safety handling.
404 """
405
406 - def __init__(self, personality = None):
407 if personality is None:
408 raise gmExceptions.ConstructorError, 'must specify personality'
409 self.__personality = personality
410 self.__attached = 0
411 self._get_source_personality = None
412 self.__user_done = False
413 self.__user_answer = 'no answer yet'
414 self.__pat = gmPerson.gmCurrentPatient()
415
416 self.__auth_cookie = str(random.random())
417 self.__pat_lock_cookie = str(random.random())
418 self.__lock_after_load_cookie = str(random.random())
419
420 _log.info('slave mode personality is [%s]', personality)
421
422
423
424 - def attach(self, personality = None):
425 if self.__attached:
426 _log.error('attach with [%s] rejected, already serving a client', personality)
427 return (0, _('attach rejected, already serving a client'))
428 if personality != self.__personality:
429 _log.error('rejecting attach to personality [%s], only servicing [%s]' % (personality, self.__personality))
430 return (0, _('attach to personality [%s] rejected') % personality)
431 self.__attached = 1
432 self.__auth_cookie = str(random.random())
433 return (1, self.__auth_cookie)
434
435 - def detach(self, auth_cookie=None):
436 if not self.__attached:
437 return 1
438 if auth_cookie != self.__auth_cookie:
439 _log.error('rejecting detach() with cookie [%s]' % auth_cookie)
440 return 0
441 self.__attached = 0
442 return 1
443
445 if not self.__attached:
446 return 1
447 self.__user_done = False
448
449 wx.CallAfter(self._force_detach)
450 return 1
451
453 ver = _cfg.get(option = u'client_version')
454 return "GNUmed %s, %s $Revision: 1.50 $" % (ver, self.__class__.__name__)
455
457 """Shuts down this client instance."""
458 if not self.__attached:
459 return 0
460 if auth_cookie != self.__auth_cookie:
461 _log.error('non-authenticated shutdown_gnumed()')
462 return 0
463 wx.CallAfter(self._shutdown_gnumed, forced)
464 return 1
465
467 """Raise ourselves to the top of the desktop."""
468 if not self.__attached:
469 return 0
470 if auth_cookie != self.__auth_cookie:
471 _log.error('non-authenticated raise_gnumed()')
472 return 0
473 return "cMacroPrimitives.raise_gnumed() not implemented"
474
476 if not self.__attached:
477 return 0
478 if auth_cookie != self.__auth_cookie:
479 _log.error('non-authenticated get_loaded_plugins()')
480 return 0
481 gb = gmGuiBroker.GuiBroker()
482 return gb['horstspace.notebook.gui'].keys()
483
485 """Raise a notebook plugin within GNUmed."""
486 if not self.__attached:
487 return 0
488 if auth_cookie != self.__auth_cookie:
489 _log.error('non-authenticated raise_notebook_plugin()')
490 return 0
491
492 wx.CallAfter(gmPlugin.raise_notebook_plugin, a_plugin)
493 return 1
494
496 """Load external patient, perhaps create it.
497
498 Callers must use get_user_answer() to get status information.
499 It is unsafe to proceed without knowing the completion state as
500 the controlled client may be waiting for user input from a
501 patient selection list.
502 """
503 if not self.__attached:
504 return (0, _('request rejected, you are not attach()ed'))
505 if auth_cookie != self.__auth_cookie:
506 _log.error('non-authenticated load_patient_from_external_source()')
507 return (0, _('rejected load_patient_from_external_source(), not authenticated'))
508 if self.__pat.locked:
509 _log.error('patient is locked, cannot load from external source')
510 return (0, _('current patient is locked'))
511 self.__user_done = False
512 wx.CallAfter(self._load_patient_from_external_source)
513 self.__lock_after_load_cookie = str(random.random())
514 return (1, self.__lock_after_load_cookie)
515
517 if not self.__attached:
518 return (0, _('request rejected, you are not attach()ed'))
519 if auth_cookie != self.__auth_cookie:
520 _log.error('non-authenticated lock_load_patient()')
521 return (0, _('rejected lock_load_patient(), not authenticated'))
522
523 if lock_after_load_cookie != self.__lock_after_load_cookie:
524 _log.warning('patient lock-after-load request rejected due to wrong cookie [%s]' % lock_after_load_cookie)
525 return (0, 'patient lock-after-load request rejected, wrong cookie provided')
526 self.__pat.locked = True
527 self.__pat_lock_cookie = str(random.random())
528 return (1, self.__pat_lock_cookie)
529
531 if not self.__attached:
532 return (0, _('request rejected, you are not attach()ed'))
533 if auth_cookie != self.__auth_cookie:
534 _log.error('non-authenticated lock_into_patient()')
535 return (0, _('rejected lock_into_patient(), not authenticated'))
536 if self.__pat.locked:
537 _log.error('patient is already locked')
538 return (0, _('already locked into a patient'))
539 searcher = gmPerson.cPatientSearcher_SQL()
540 if type(search_params) == types.DictType:
541 idents = searcher.get_identities(search_dict=search_params)
542 print "must use dto, not search_dict"
543 print xxxxxxxxxxxxxxxxx
544 else:
545 idents = searcher.get_identities(search_term=search_params)
546 if idents is None:
547 return (0, _('error searching for patient with [%s]/%s') % (search_term, search_dict))
548 if len(idents) == 0:
549 return (0, _('no patient found for [%s]/%s') % (search_term, search_dict))
550
551 if len(idents) > 1:
552 return (0, _('several matching patients found for [%s]/%s') % (search_term, search_dict))
553 if not gmPatSearchWidgets.set_active_patient(patient = idents[0]):
554 return (0, _('cannot activate patient [%s] (%s/%s)') % (str(idents[0]), search_term, search_dict))
555 self.__pat.locked = True
556 self.__pat_lock_cookie = str(random.random())
557 return (1, self.__pat_lock_cookie)
558
560 if not self.__attached:
561 return (0, _('request rejected, you are not attach()ed'))
562 if auth_cookie != self.__auth_cookie:
563 _log.error('non-authenticated unlock_patient()')
564 return (0, _('rejected unlock_patient, not authenticated'))
565
566 if not self.__pat.locked:
567 return (1, '')
568
569 if unlock_cookie != self.__pat_lock_cookie:
570 _log.warning('patient unlock request rejected due to wrong cookie [%s]' % unlock_cookie)
571 return (0, 'patient unlock request rejected, wrong cookie provided')
572 self.__pat.locked = False
573 return (1, '')
574
576 if not self.__attached:
577 return 0
578 if auth_cookie != self.__auth_cookie:
579 _log.error('non-authenticated select_identity()')
580 return 0
581 return "cMacroPrimitives.assume_staff_identity() not implemented"
582
584 if not self.__user_done:
585 return (0, 'still waiting')
586 self.__user_done = False
587 return (1, self.__user_answer)
588
589
590
592 msg = _(
593 'Someone tries to forcibly break the existing\n'
594 'controlling connection. This may or may not\n'
595 'have legitimate reasons.\n\n'
596 'Do you want to allow breaking the connection ?'
597 )
598 can_break_conn = gmGuiHelpers.gm_show_question (
599 aMessage = msg,
600 aTitle = _('forced detach attempt')
601 )
602 if can_break_conn:
603 self.__user_answer = 1
604 else:
605 self.__user_answer = 0
606 self.__user_done = True
607 if can_break_conn:
608 self.__pat.locked = False
609 self.__attached = 0
610 return 1
611
613 top_win = wx.GetApp().GetTopWindow()
614 if forced:
615 top_win.Destroy()
616 else:
617 top_win.Close()
618
627
628
629
630 if __name__ == '__main__':
631
632 gmI18N.activate_locale()
633 gmI18N.install_domain()
634
635
637 handler = gmPlaceholderHandler()
638 handler.debug = True
639
640 for placeholder in ['a', 'b']:
641 print handler[placeholder]
642
643 pat = gmPerson.ask_for_patient()
644 if pat is None:
645 return
646
647 gmPatSearchWidgets.set_active_patient(patient = pat)
648
649 print 'DOB (YYYY-MM-DD):', handler['date_of_birth::%Y-%m-%d']
650
651 app = wx.PyWidgetTester(size = (200, 50))
652 for placeholder in known_placeholders:
653 print placeholder, "=", handler[placeholder]
654
655 ph = 'progress_notes::ap'
656 print '%s: %s' % (ph, handler[ph])
657
659
660 tests = [
661
662 '$<lastname>$',
663 '$<lastname::::3>$',
664
665
666 'lastname',
667 '$<lastname',
668 '$<lastname::',
669 '$<lastname::>$',
670 '$<lastname::abc>$',
671 '$<lastname::abc::>$',
672 '$<lastname::abc::3>$',
673 '$<lastname::abc::xyz>$',
674 '$<lastname::::>$',
675 '$<lastname::::xyz>$',
676
677 '$<date_of_birth::%Y-%m-%d>$',
678 '$<date_of_birth::%Y-%m-%d::3>$',
679 '$<date_of_birth::%Y-%m-%d::>$',
680
681
682 '$<adr_location::home::35>$',
683 '$<gender_mapper::male//female//other::5>$',
684 '$<current_meds::==> %(brand)s %(preparation)s (%(substance)s) <==\n::50>$',
685 '$<allergy_list::%(descriptor)s, >$'
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700 ]
701
702 pat = gmPerson.ask_for_patient()
703 if pat is None:
704 return
705
706 gmPatSearchWidgets.set_active_patient(patient = pat)
707
708 handler = gmPlaceholderHandler()
709 handler.debug = True
710
711 for placeholder in tests:
712 print placeholder, "=>", handler[placeholder]
713 print "--------------"
714 raw_input()
715
716
717
718
719
720
721
722
723
724
725
727 from Gnumed.pycommon import gmScriptingListener
728 import xmlrpclib
729 listener = gmScriptingListener.cScriptingListener(macro_executor = cMacroPrimitives(personality='unit test'), port=9999)
730
731 s = xmlrpclib.ServerProxy('http://localhost:9999')
732 print "should fail:", s.attach()
733 print "should fail:", s.attach('wrong cookie')
734 print "should work:", s.version()
735 print "should fail:", s.raise_gnumed()
736 print "should fail:", s.raise_notebook_plugin('test plugin')
737 print "should fail:", s.lock_into_patient('kirk, james')
738 print "should fail:", s.unlock_patient()
739 status, conn_auth = s.attach('unit test')
740 print "should work:", status, conn_auth
741 print "should work:", s.version()
742 print "should work:", s.raise_gnumed(conn_auth)
743 status, pat_auth = s.lock_into_patient(conn_auth, 'kirk, james')
744 print "should work:", status, pat_auth
745 print "should fail:", s.unlock_patient(conn_auth, 'bogus patient unlock cookie')
746 print "should work", s.unlock_patient(conn_auth, pat_auth)
747 data = {'firstname': 'jame', 'lastnames': 'Kirk', 'gender': 'm'}
748 status, pat_auth = s.lock_into_patient(conn_auth, data)
749 print "should work:", status, pat_auth
750 print "should work", s.unlock_patient(conn_auth, pat_auth)
751 print s.detach('bogus detach cookie')
752 print s.detach(conn_auth)
753 del s
754
755 listener.shutdown()
756
757
758 if len(sys.argv) > 1 and sys.argv[1] == 'test':
759
760 test_new_variant_placeholders()
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
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