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