1 """GNUmed macro primitives.
2
3 This module implements functions a macro can legally use.
4 """
5
6 __version__ = "$Revision: 1.51 $"
7 __author__ = "K.Hilbert <karsten.hilbert@gmx.net>"
8
9 import sys, time, random, types, logging
10
11
12 import wx
13
14
15 if __name__ == '__main__':
16 sys.path.insert(0, '../../')
17 from Gnumed.pycommon import gmI18N, gmGuiBroker, gmExceptions, gmBorg, gmTools
18 from Gnumed.pycommon import gmCfg2, gmDateTime
19 from Gnumed.business import gmPerson, gmDemographicRecord, gmMedication, gmPathLab
20 from Gnumed.wxpython import gmGuiHelpers, gmPlugin, gmPatSearchWidgets, gmNarrativeWidgets
21
22
23 _log = logging.getLogger('gm.scripting')
24 _cfg = gmCfg2.gmCfgData()
25
26
27 known_placeholders = [
28 'lastname',
29 'firstname',
30 'title',
31 'date_of_birth',
32 'progress_notes',
33 'soap',
34 'soap_s',
35 'soap_o',
36 'soap_a',
37 'soap_p',
38 u'client_version',
39 u'current_provider',
40 u'allergy_state'
41 ]
42
43
44
45 known_variant_placeholders = [
46 u'soap',
47 u'progress_notes',
48 u'date_of_birth',
49 u'adr_street',
50 u'adr_number',
51 u'adr_location',
52 u'adr_postcode',
53 u'gender_mapper',
54 u'current_meds',
55 u'current_meds_table',
56 u'current_meds_notes',
57 u'lab_table',
58 u'today',
59 u'tex_escape',
60 u'allergies',
61 u'allergy_list',
62 u'problems',
63 u'name'
64 ]
65
66 default_placeholder_regex = r'\$<.+?>\$'
67
68
69
70
71
72
73
74
75 default_placeholder_start = u'$<'
76 default_placeholder_end = u'>$'
77
79 """Replaces placeholders in forms, fields, etc.
80
81 - patient related placeholders operate on the currently active patient
82 - is passed to the forms handling code, for example
83
84 Note that this cannot be called from a non-gui thread unless
85 wrapped in wx.CallAfter.
86
87 There are currently three types of placeholders:
88
89 simple static placeholders
90 - those are listed in known_placeholders
91 - they are used as-is
92
93 extended static placeholders
94 - those are like the static ones but have "::::<NUMBER>" appended
95 where <NUMBER> is the maximum length
96
97 variant placeholders
98 - those are listed in known_variant_placeholders
99 - they are parsed into placeholder, data, and maximum length
100 - the length is optional
101 - data is passed to the handler
102 """
104
105 self.pat = gmPerson.gmCurrentPatient()
106 self.debug = False
107
108 self.invalid_placeholder_template = _('invalid placeholder [%s]')
109
110
111
113 """Map self['placeholder'] to self.placeholder.
114
115 This is useful for replacing placeholders parsed out
116 of documents as strings.
117
118 Unknown/invalid placeholders still deliver a result but
119 it will be glaringly obvious if debugging is enabled.
120 """
121 _log.debug('replacing [%s]', placeholder)
122
123 original_placeholder = placeholder
124
125 if placeholder.startswith(default_placeholder_start):
126 placeholder = placeholder[len(default_placeholder_start):]
127 if placeholder.endswith(default_placeholder_end):
128 placeholder = placeholder[:-len(default_placeholder_end)]
129 else:
130 _log.debug('placeholder must either start with [%s] and end with [%s] or neither of both', default_placeholder_start, default_placeholder_end)
131 if self.debug:
132 return self.invalid_placeholder_template % original_placeholder
133 return None
134
135
136 if placeholder in known_placeholders:
137 return getattr(self, placeholder)
138
139
140 parts = placeholder.split('::::', 1)
141 if len(parts) == 2:
142 name, lng = parts
143 try:
144 return getattr(self, name)[:int(lng)]
145 except:
146 _log.exception('placeholder handling error: %s', original_placeholder)
147 if self.debug:
148 return self.invalid_placeholder_template % original_placeholder
149 return None
150
151
152 parts = placeholder.split('::', 2)
153 if len(parts) == 2:
154 name, data = parts
155 lng = None
156 elif len(parts) == 3:
157 name, data, lng = parts
158 try:
159 lng = int(lng)
160 except:
161 _log.exception('placeholder length definition error: %s, discarding length', original_placeholder)
162 lng = None
163 else:
164 _log.warning('invalid placeholder layout: %s', original_placeholder)
165 if self.debug:
166 return self.invalid_placeholder_template % original_placeholder
167 return None
168
169 handler = getattr(self, '_get_variant_%s' % name, None)
170 if handler is None:
171 _log.warning('no handler <_get_variant_%s> for placeholder %s', name, original_placeholder)
172 if self.debug:
173 return self.invalid_placeholder_template % original_placeholder
174 return None
175
176 try:
177 if lng is None:
178 return handler(data = data)
179 return handler(data = data)[:lng]
180 except:
181 _log.exception('placeholder handling error: %s', original_placeholder)
182 if self.debug:
183 return self.invalid_placeholder_template % original_placeholder
184 return None
185
186 _log.error('something went wrong, should never get here')
187 return None
188
189
190
191
192
194 """This does nothing, used as a NOOP properties setter."""
195 pass
196
199
202
205
207 return self._get_variant_date_of_birth(data='%x')
208
210 return self._get_variant_soap()
211
213 return self._get_variant_soap(data = u's')
214
216 return self._get_variant_soap(data = u'o')
217
219 return self._get_variant_soap(data = u'a')
220
222 return self._get_variant_soap(data = u'p')
223
225 return self._get_variant_soap(soap_cats = None)
226
228 return gmTools.coalesce (
229 _cfg.get(option = u'client_version'),
230 u'%s' % self.__class__.__name__
231 )
232
248
250 allg_state = self.pat.get_emr().allergy_state
251
252 if allg_state['last_confirmed'] is None:
253 date_confirmed = u''
254 else:
255 date_confirmed = u' (%s)' % allg_state['last_confirmed'].strftime('%Y %B %d').decode(gmI18N.get_encoding())
256
257 tmp = u'%s%s' % (
258 allg_state.state_string,
259 date_confirmed
260 )
261 return tmp
262
263
264
265 placeholder_regex = property(lambda x: default_placeholder_regex, _setter_noop)
266
267
268 lastname = property(_get_lastname, _setter_noop)
269 firstname = property(_get_firstname, _setter_noop)
270 title = property(_get_title, _setter_noop)
271 date_of_birth = property(_get_dob, _setter_noop)
272
273 progress_notes = property(_get_progress_notes, _setter_noop)
274 soap = property(_get_progress_notes, _setter_noop)
275 soap_s = property(_get_soap_s, _setter_noop)
276 soap_o = property(_get_soap_o, _setter_noop)
277 soap_a = property(_get_soap_a, _setter_noop)
278 soap_p = property(_get_soap_p, _setter_noop)
279 soap_admin = property(_get_soap_admin, _setter_noop)
280
281 allergy_state = property(_get_allergy_state, _setter_noop)
282
283 client_version = property(_get_client_version, _setter_noop)
284
285 current_provider = property(_get_current_provider, _setter_noop)
286
287
288
290 return self._get_variant_soap(data=data)
291
293 if data is None:
294 cats = list(data)
295 template = u'%s'
296 else:
297 parts = data.split('//', 2)
298 if len(parts) == 1:
299 cats = list(parts)
300 template = u'%s'
301 else:
302 cats = list(parts[0])
303 template = parts[1]
304
305 narr = gmNarrativeWidgets.select_narrative_from_episodes(soap_cats = cats)
306
307 if len(narr) == 0:
308 return u''
309
310 narr = [ template % n['narrative'] for n in narr ]
311
312 return u'\n'.join(narr)
313
332
335
336
338 values = data.split('//', 2)
339
340 if len(values) == 2:
341 male_value, female_value = values
342 other_value = u'<unkown gender>'
343 elif len(values) == 3:
344 male_value, female_value, other_value = values
345 else:
346 return _('invalid gender mapping layout: [%s]') % data
347
348 if self.pat['gender'] == u'm':
349 return male_value
350
351 if self.pat['gender'] == u'f':
352 return female_value
353
354 return other_value
355
357
358
359 adrs = self.pat.get_addresses(address_type=data)
360 if len(adrs) == 0:
361 return _('no street for address type [%s]') % data
362 return adrs[0]['street']
363
365 adrs = self.pat.get_addresses(address_type=data)
366 if len(adrs) == 0:
367 return _('no number for address type [%s]') % data
368 return adrs[0]['number']
369
371 adrs = self.pat.get_addresses(address_type=data)
372 if len(adrs) == 0:
373 return _('no location for address type [%s]') % data
374 return adrs[0]['urb']
375
376 - def _get_variant_adr_postcode(self, data=u'?'):
377 adrs = self.pat.get_addresses(address_type=data)
378 if len(adrs) == 0:
379 return _('no postcode for address type [%s]') % data
380 return adrs[0]['postcode']
381
383 if data is None:
384 return [_('template is missing')]
385
386 template, separator = data.split('//', 2)
387
388 emr = self.pat.get_emr()
389 return separator.join([ template % 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 return u'\n'.join([ data % a for a in emr.get_allergies() ])
398
400
401 if data is None:
402 return [_('template is missing')]
403
404 emr = self.pat.get_emr()
405 current_meds = emr.get_current_substance_intake (
406 include_inactive = False,
407 include_unapproved = False,
408 order_by = u'brand, substance'
409 )
410
411
412
413 return u'\n'.join([ data % m for m in current_meds ])
414
416
417 options = data.split('//')
418
419 if u'latex' in options:
420 return gmMedication.format_substance_intake (
421 emr = self.pat.get_emr(),
422 output_format = u'latex',
423 table_type = u'by-brand'
424 )
425
426 _log.error('no known current medications table formatting style in [%]', data)
427 return _('unknown current medication table formatting style')
428
430
431 options = data.split('//')
432
433 if u'latex' in options:
434 return gmMedication.format_substance_intake_notes (
435 emr = self.pat.get_emr(),
436 output_format = u'latex',
437 table_type = u'by-brand'
438 )
439
440 _log.error('no known current medications notes formatting style in [%]', data)
441 return _('unknown current medication notes formatting style')
442
457
459
460 if data is None:
461 return [_('template is missing')]
462
463 probs = self.pat.get_emr().get_problems()
464
465 return u'\n'.join([ data % p for p in probs ])
466
469
472
473
474
475
476
478 """Functions a macro can legally use.
479
480 An instance of this class is passed to the GNUmed scripting
481 listener. Hence, all actions a macro can legally take must
482 be defined in this class. Thus we achieve some screening for
483 security and also thread safety handling.
484 """
485
486 - def __init__(self, personality = None):
487 if personality is None:
488 raise gmExceptions.ConstructorError, 'must specify personality'
489 self.__personality = personality
490 self.__attached = 0
491 self._get_source_personality = None
492 self.__user_done = False
493 self.__user_answer = 'no answer yet'
494 self.__pat = gmPerson.gmCurrentPatient()
495
496 self.__auth_cookie = str(random.random())
497 self.__pat_lock_cookie = str(random.random())
498 self.__lock_after_load_cookie = str(random.random())
499
500 _log.info('slave mode personality is [%s]', personality)
501
502
503
504 - def attach(self, personality = None):
505 if self.__attached:
506 _log.error('attach with [%s] rejected, already serving a client', personality)
507 return (0, _('attach rejected, already serving a client'))
508 if personality != self.__personality:
509 _log.error('rejecting attach to personality [%s], only servicing [%s]' % (personality, self.__personality))
510 return (0, _('attach to personality [%s] rejected') % personality)
511 self.__attached = 1
512 self.__auth_cookie = str(random.random())
513 return (1, self.__auth_cookie)
514
515 - def detach(self, auth_cookie=None):
516 if not self.__attached:
517 return 1
518 if auth_cookie != self.__auth_cookie:
519 _log.error('rejecting detach() with cookie [%s]' % auth_cookie)
520 return 0
521 self.__attached = 0
522 return 1
523
525 if not self.__attached:
526 return 1
527 self.__user_done = False
528
529 wx.CallAfter(self._force_detach)
530 return 1
531
533 ver = _cfg.get(option = u'client_version')
534 return "GNUmed %s, %s $Revision: 1.51 $" % (ver, self.__class__.__name__)
535
537 """Shuts down this client instance."""
538 if not self.__attached:
539 return 0
540 if auth_cookie != self.__auth_cookie:
541 _log.error('non-authenticated shutdown_gnumed()')
542 return 0
543 wx.CallAfter(self._shutdown_gnumed, forced)
544 return 1
545
547 """Raise ourselves to the top of the desktop."""
548 if not self.__attached:
549 return 0
550 if auth_cookie != self.__auth_cookie:
551 _log.error('non-authenticated raise_gnumed()')
552 return 0
553 return "cMacroPrimitives.raise_gnumed() not implemented"
554
556 if not self.__attached:
557 return 0
558 if auth_cookie != self.__auth_cookie:
559 _log.error('non-authenticated get_loaded_plugins()')
560 return 0
561 gb = gmGuiBroker.GuiBroker()
562 return gb['horstspace.notebook.gui'].keys()
563
565 """Raise a notebook plugin within GNUmed."""
566 if not self.__attached:
567 return 0
568 if auth_cookie != self.__auth_cookie:
569 _log.error('non-authenticated raise_notebook_plugin()')
570 return 0
571
572 wx.CallAfter(gmPlugin.raise_notebook_plugin, a_plugin)
573 return 1
574
576 """Load external patient, perhaps create it.
577
578 Callers must use get_user_answer() to get status information.
579 It is unsafe to proceed without knowing the completion state as
580 the controlled client may be waiting for user input from a
581 patient selection list.
582 """
583 if not self.__attached:
584 return (0, _('request rejected, you are not attach()ed'))
585 if auth_cookie != self.__auth_cookie:
586 _log.error('non-authenticated load_patient_from_external_source()')
587 return (0, _('rejected load_patient_from_external_source(), not authenticated'))
588 if self.__pat.locked:
589 _log.error('patient is locked, cannot load from external source')
590 return (0, _('current patient is locked'))
591 self.__user_done = False
592 wx.CallAfter(self._load_patient_from_external_source)
593 self.__lock_after_load_cookie = str(random.random())
594 return (1, self.__lock_after_load_cookie)
595
597 if not self.__attached:
598 return (0, _('request rejected, you are not attach()ed'))
599 if auth_cookie != self.__auth_cookie:
600 _log.error('non-authenticated lock_load_patient()')
601 return (0, _('rejected lock_load_patient(), not authenticated'))
602
603 if lock_after_load_cookie != self.__lock_after_load_cookie:
604 _log.warning('patient lock-after-load request rejected due to wrong cookie [%s]' % lock_after_load_cookie)
605 return (0, 'patient lock-after-load request rejected, wrong cookie provided')
606 self.__pat.locked = True
607 self.__pat_lock_cookie = str(random.random())
608 return (1, self.__pat_lock_cookie)
609
611 if not self.__attached:
612 return (0, _('request rejected, you are not attach()ed'))
613 if auth_cookie != self.__auth_cookie:
614 _log.error('non-authenticated lock_into_patient()')
615 return (0, _('rejected lock_into_patient(), not authenticated'))
616 if self.__pat.locked:
617 _log.error('patient is already locked')
618 return (0, _('already locked into a patient'))
619 searcher = gmPerson.cPatientSearcher_SQL()
620 if type(search_params) == types.DictType:
621 idents = searcher.get_identities(search_dict=search_params)
622 print "must use dto, not search_dict"
623 print xxxxxxxxxxxxxxxxx
624 else:
625 idents = searcher.get_identities(search_term=search_params)
626 if idents is None:
627 return (0, _('error searching for patient with [%s]/%s') % (search_term, search_dict))
628 if len(idents) == 0:
629 return (0, _('no patient found for [%s]/%s') % (search_term, search_dict))
630
631 if len(idents) > 1:
632 return (0, _('several matching patients found for [%s]/%s') % (search_term, search_dict))
633 if not gmPatSearchWidgets.set_active_patient(patient = idents[0]):
634 return (0, _('cannot activate patient [%s] (%s/%s)') % (str(idents[0]), search_term, search_dict))
635 self.__pat.locked = True
636 self.__pat_lock_cookie = str(random.random())
637 return (1, self.__pat_lock_cookie)
638
640 if not self.__attached:
641 return (0, _('request rejected, you are not attach()ed'))
642 if auth_cookie != self.__auth_cookie:
643 _log.error('non-authenticated unlock_patient()')
644 return (0, _('rejected unlock_patient, not authenticated'))
645
646 if not self.__pat.locked:
647 return (1, '')
648
649 if unlock_cookie != self.__pat_lock_cookie:
650 _log.warning('patient unlock request rejected due to wrong cookie [%s]' % unlock_cookie)
651 return (0, 'patient unlock request rejected, wrong cookie provided')
652 self.__pat.locked = False
653 return (1, '')
654
656 if not self.__attached:
657 return 0
658 if auth_cookie != self.__auth_cookie:
659 _log.error('non-authenticated select_identity()')
660 return 0
661 return "cMacroPrimitives.assume_staff_identity() not implemented"
662
664 if not self.__user_done:
665 return (0, 'still waiting')
666 self.__user_done = False
667 return (1, self.__user_answer)
668
669
670
672 msg = _(
673 'Someone tries to forcibly break the existing\n'
674 'controlling connection. This may or may not\n'
675 'have legitimate reasons.\n\n'
676 'Do you want to allow breaking the connection ?'
677 )
678 can_break_conn = gmGuiHelpers.gm_show_question (
679 aMessage = msg,
680 aTitle = _('forced detach attempt')
681 )
682 if can_break_conn:
683 self.__user_answer = 1
684 else:
685 self.__user_answer = 0
686 self.__user_done = True
687 if can_break_conn:
688 self.__pat.locked = False
689 self.__attached = 0
690 return 1
691
693 top_win = wx.GetApp().GetTopWindow()
694 if forced:
695 top_win.Destroy()
696 else:
697 top_win.Close()
698
707
708
709
710 if __name__ == '__main__':
711
712 if len(sys.argv) < 2:
713 sys.exit()
714
715 if sys.argv[1] != 'test':
716 sys.exit()
717
718 gmI18N.activate_locale()
719 gmI18N.install_domain()
720
721
723 handler = gmPlaceholderHandler()
724 handler.debug = True
725
726 for placeholder in ['a', 'b']:
727 print handler[placeholder]
728
729 pat = gmPerson.ask_for_patient()
730 if pat is None:
731 return
732
733 gmPatSearchWidgets.set_active_patient(patient = pat)
734
735 print 'DOB (YYYY-MM-DD):', handler['date_of_birth::%Y-%m-%d']
736
737 app = wx.PyWidgetTester(size = (200, 50))
738 for placeholder in known_placeholders:
739 print placeholder, "=", handler[placeholder]
740
741 ph = 'progress_notes::ap'
742 print '%s: %s' % (ph, handler[ph])
743
745
746 tests = [
747
748 '$<lastname>$',
749 '$<lastname::::3>$',
750 '$<name::%(title)s %(firstnames)s%(preferred)s%(lastnames)s>$',
751
752
753 'lastname',
754 '$<lastname',
755 '$<lastname::',
756 '$<lastname::>$',
757 '$<lastname::abc>$',
758 '$<lastname::abc::>$',
759 '$<lastname::abc::3>$',
760 '$<lastname::abc::xyz>$',
761 '$<lastname::::>$',
762 '$<lastname::::xyz>$',
763
764 '$<date_of_birth::%Y-%m-%d>$',
765 '$<date_of_birth::%Y-%m-%d::3>$',
766 '$<date_of_birth::%Y-%m-%d::>$',
767
768
769 '$<adr_location::home::35>$',
770 '$<gender_mapper::male//female//other::5>$',
771 '$<current_meds::==> %(brand)s %(preparation)s (%(substance)s) <==\n::50>$',
772 '$<allergy_list::%(descriptor)s, >$',
773 '$<current_meds_table::latex//by-brand>$'
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788 ]
789
790 pat = gmPerson.ask_for_patient()
791 if pat is None:
792 return
793
794 gmPatSearchWidgets.set_active_patient(patient = pat)
795
796 handler = gmPlaceholderHandler()
797 handler.debug = True
798
799 for placeholder in tests:
800 print placeholder, "=>", handler[placeholder]
801 print "--------------"
802 raw_input()
803
804
805
806
807
808
809
810
811
812
813
815 from Gnumed.pycommon import gmScriptingListener
816 import xmlrpclib
817 listener = gmScriptingListener.cScriptingListener(macro_executor = cMacroPrimitives(personality='unit test'), port=9999)
818
819 s = xmlrpclib.ServerProxy('http://localhost:9999')
820 print "should fail:", s.attach()
821 print "should fail:", s.attach('wrong cookie')
822 print "should work:", s.version()
823 print "should fail:", s.raise_gnumed()
824 print "should fail:", s.raise_notebook_plugin('test plugin')
825 print "should fail:", s.lock_into_patient('kirk, james')
826 print "should fail:", s.unlock_patient()
827 status, conn_auth = s.attach('unit test')
828 print "should work:", status, conn_auth
829 print "should work:", s.version()
830 print "should work:", s.raise_gnumed(conn_auth)
831 status, pat_auth = s.lock_into_patient(conn_auth, 'kirk, james')
832 print "should work:", status, pat_auth
833 print "should fail:", s.unlock_patient(conn_auth, 'bogus patient unlock cookie')
834 print "should work", s.unlock_patient(conn_auth, pat_auth)
835 data = {'firstname': 'jame', 'lastnames': 'Kirk', 'gender': 'm'}
836 status, pat_auth = s.lock_into_patient(conn_auth, data)
837 print "should work:", status, pat_auth
838 print "should work", s.unlock_patient(conn_auth, pat_auth)
839 print s.detach('bogus detach cookie')
840 print s.detach(conn_auth)
841 del s
842
843 listener.shutdown()
844
846
847 import re as regex
848
849 tests = [
850 ' $<lastname>$ ',
851 ' $<lastname::::3>$ ',
852
853
854 '$<date_of_birth::%Y-%m-%d>$',
855 '$<date_of_birth::%Y-%m-%d::3>$',
856 '$<date_of_birth::%Y-%m-%d::>$',
857
858 '$<adr_location::home::35>$',
859 '$<gender_mapper::male//female//other::5>$',
860 '$<current_meds::==> %(brand)s %(preparation)s (%(substance)s) <==\\n::50>$',
861 '$<allergy_list::%(descriptor)s, >$',
862
863 '\\noindent Patient: $<lastname>$, $<firstname>$',
864 '$<allergies::%(descriptor)s & %(l10n_type)s & {\\footnotesize %(reaction)s} \tabularnewline \hline >$',
865 '$<current_meds:: \item[%(substance)s] {\\footnotesize (%(brand)s)} %(preparation)s %(strength)s: %(schedule)s >$'
866 ]
867
868 tests = [
869
870 'junk $<lastname::::3>$ junk',
871 'junk $<lastname::abc::3>$ junk',
872 'junk $<lastname::abc>$ junk',
873 'junk $<lastname>$ junk',
874
875 'junk $<lastname>$ junk $<firstname>$ junk',
876 'junk $<lastname::abc>$ junk $<fiststname::abc>$ junk',
877 'junk $<lastname::abc::3>$ junk $<firstname::abc::3>$ junk',
878 'junk $<lastname::::3>$ junk $<firstname::::3>$ junk'
879
880 ]
881
882 print "testing placeholder regex:", default_placeholder_regex
883 print ""
884
885 for t in tests:
886 print 'line: "%s"' % t
887 print "placeholders:"
888 for p in regex.findall(default_placeholder_regex, t, regex.IGNORECASE):
889 print ' => "%s"' % p
890 print " "
891
892
893
894 test_new_variant_placeholders()
895
896
897
898
899