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
249 allg_state = self.pat.get_emr().allergy_state
250
251 if allg_state['last_confirmed'] is None:
252 date_confirmed = u''
253 else:
254 date_confirmed = u' (%s)' % allg_state['last_confirmed'].strftime('%Y %B %d').decode(gmI18N.get_encoding())
255
256 tmp = u'%s%s' % (
257 allg_state.state_string,
258 date_confirmed
259 )
260 return tmp
261
262
263
264 placeholder_regex = property(lambda x: default_placeholder_regex, _setter_noop)
265
266
267 lastname = property(_get_lastname, _setter_noop)
268 firstname = property(_get_firstname, _setter_noop)
269 title = property(_get_title, _setter_noop)
270 date_of_birth = property(_get_dob, _setter_noop)
271
272 progress_notes = property(_get_progress_notes, _setter_noop)
273 soap = property(_get_progress_notes, _setter_noop)
274 soap_s = property(_get_soap_s, _setter_noop)
275 soap_o = property(_get_soap_o, _setter_noop)
276 soap_a = property(_get_soap_a, _setter_noop)
277 soap_p = property(_get_soap_p, _setter_noop)
278 soap_admin = property(_get_soap_admin, _setter_noop)
279
280 allergy_state = property(_get_allergy_state, _setter_noop)
281
282 client_version = property(_get_client_version, _setter_noop)
283
284 current_provider = property(_get_current_provider, _setter_noop)
285
286
287
289 return self._get_variant_soap(data=data)
290
292 if data is None:
293 cats = list(data)
294 template = u'%s'
295 else:
296 parts = data.split('//', 2)
297 if len(parts) == 1:
298 cats = list(parts)
299 template = u'%s'
300 else:
301 cats = list(parts[0])
302 template = parts[1]
303
304 narr = gmNarrativeWidgets.select_narrative_from_episodes(soap_cats = cats)
305
306 if len(narr) == 0:
307 return u''
308
309 narr = [ template % n['narrative'] for n in narr ]
310
311 return u'\n'.join(narr)
312
331
334
335
337 values = data.split('//', 2)
338
339 if len(values) == 2:
340 male_value, female_value = values
341 other_value = u'<unkown gender>'
342 elif len(values) == 3:
343 male_value, female_value, other_value = values
344 else:
345 return _('invalid gender mapping layout: [%s]') % data
346
347 if self.pat['gender'] == u'm':
348 return male_value
349
350 if self.pat['gender'] == u'f':
351 return female_value
352
353 return other_value
354
356
357
358 adrs = self.pat.get_addresses(address_type=data)
359 if len(adrs) == 0:
360 return _('no street for address type [%s]') % data
361 return adrs[0]['street']
362
364 adrs = self.pat.get_addresses(address_type=data)
365 if len(adrs) == 0:
366 return _('no number for address type [%s]') % data
367 return adrs[0]['number']
368
370 adrs = self.pat.get_addresses(address_type=data)
371 if len(adrs) == 0:
372 return _('no location for address type [%s]') % data
373 return adrs[0]['urb']
374
375 - def _get_variant_adr_postcode(self, data=u'?'):
376 adrs = self.pat.get_addresses(address_type=data)
377 if len(adrs) == 0:
378 return _('no postcode for address type [%s]') % data
379 return adrs[0]['postcode']
380
382 if data is None:
383 return [_('template is missing')]
384
385 template, separator = data.split('//', 2)
386
387 emr = self.pat.get_emr()
388 return separator.join([ template % a for a in emr.get_allergies() ])
389
391
392 if data is None:
393 return [_('template is missing')]
394
395 emr = self.pat.get_emr()
396 return u'\n'.join([ data % a for a in emr.get_allergies() ])
397
399
400 if data is None:
401 return [_('template is missing')]
402
403 emr = self.pat.get_emr()
404 current_meds = emr.get_current_substance_intake (
405 include_inactive = False,
406 include_unapproved = False,
407 order_by = u'brand, substance'
408 )
409
410
411
412 return u'\n'.join([ data % m for m in current_meds ])
413
415
416 options = data.split('//')
417
418 if u'latex' in options:
419 return gmMedication.format_substance_intake (
420 emr = self.pat.get_emr(),
421 output_format = u'latex',
422 table_type = u'by-brand'
423 )
424
425 _log.error('no known current medications table formatting style in [%]', data)
426 return _('unknown current medication table formatting style')
427
442
444
445 if data is None:
446 return [_('template is missing')]
447
448 probs = self.pat.get_emr().get_problems()
449
450 return u'\n'.join([ data % p for p in probs ])
451
454
457
458
459
460
461
463 """Functions a macro can legally use.
464
465 An instance of this class is passed to the GNUmed scripting
466 listener. Hence, all actions a macro can legally take must
467 be defined in this class. Thus we achieve some screening for
468 security and also thread safety handling.
469 """
470
471 - def __init__(self, personality = None):
472 if personality is None:
473 raise gmExceptions.ConstructorError, 'must specify personality'
474 self.__personality = personality
475 self.__attached = 0
476 self._get_source_personality = None
477 self.__user_done = False
478 self.__user_answer = 'no answer yet'
479 self.__pat = gmPerson.gmCurrentPatient()
480
481 self.__auth_cookie = str(random.random())
482 self.__pat_lock_cookie = str(random.random())
483 self.__lock_after_load_cookie = str(random.random())
484
485 _log.info('slave mode personality is [%s]', personality)
486
487
488
489 - def attach(self, personality = None):
490 if self.__attached:
491 _log.error('attach with [%s] rejected, already serving a client', personality)
492 return (0, _('attach rejected, already serving a client'))
493 if personality != self.__personality:
494 _log.error('rejecting attach to personality [%s], only servicing [%s]' % (personality, self.__personality))
495 return (0, _('attach to personality [%s] rejected') % personality)
496 self.__attached = 1
497 self.__auth_cookie = str(random.random())
498 return (1, self.__auth_cookie)
499
500 - def detach(self, auth_cookie=None):
501 if not self.__attached:
502 return 1
503 if auth_cookie != self.__auth_cookie:
504 _log.error('rejecting detach() with cookie [%s]' % auth_cookie)
505 return 0
506 self.__attached = 0
507 return 1
508
510 if not self.__attached:
511 return 1
512 self.__user_done = False
513
514 wx.CallAfter(self._force_detach)
515 return 1
516
518 ver = _cfg.get(option = u'client_version')
519 return "GNUmed %s, %s $Revision: 1.51 $" % (ver, self.__class__.__name__)
520
522 """Shuts down this client instance."""
523 if not self.__attached:
524 return 0
525 if auth_cookie != self.__auth_cookie:
526 _log.error('non-authenticated shutdown_gnumed()')
527 return 0
528 wx.CallAfter(self._shutdown_gnumed, forced)
529 return 1
530
532 """Raise ourselves to the top of the desktop."""
533 if not self.__attached:
534 return 0
535 if auth_cookie != self.__auth_cookie:
536 _log.error('non-authenticated raise_gnumed()')
537 return 0
538 return "cMacroPrimitives.raise_gnumed() not implemented"
539
541 if not self.__attached:
542 return 0
543 if auth_cookie != self.__auth_cookie:
544 _log.error('non-authenticated get_loaded_plugins()')
545 return 0
546 gb = gmGuiBroker.GuiBroker()
547 return gb['horstspace.notebook.gui'].keys()
548
550 """Raise a notebook plugin within GNUmed."""
551 if not self.__attached:
552 return 0
553 if auth_cookie != self.__auth_cookie:
554 _log.error('non-authenticated raise_notebook_plugin()')
555 return 0
556
557 wx.CallAfter(gmPlugin.raise_notebook_plugin, a_plugin)
558 return 1
559
561 """Load external patient, perhaps create it.
562
563 Callers must use get_user_answer() to get status information.
564 It is unsafe to proceed without knowing the completion state as
565 the controlled client may be waiting for user input from a
566 patient selection list.
567 """
568 if not self.__attached:
569 return (0, _('request rejected, you are not attach()ed'))
570 if auth_cookie != self.__auth_cookie:
571 _log.error('non-authenticated load_patient_from_external_source()')
572 return (0, _('rejected load_patient_from_external_source(), not authenticated'))
573 if self.__pat.locked:
574 _log.error('patient is locked, cannot load from external source')
575 return (0, _('current patient is locked'))
576 self.__user_done = False
577 wx.CallAfter(self._load_patient_from_external_source)
578 self.__lock_after_load_cookie = str(random.random())
579 return (1, self.__lock_after_load_cookie)
580
582 if not self.__attached:
583 return (0, _('request rejected, you are not attach()ed'))
584 if auth_cookie != self.__auth_cookie:
585 _log.error('non-authenticated lock_load_patient()')
586 return (0, _('rejected lock_load_patient(), not authenticated'))
587
588 if lock_after_load_cookie != self.__lock_after_load_cookie:
589 _log.warning('patient lock-after-load request rejected due to wrong cookie [%s]' % lock_after_load_cookie)
590 return (0, 'patient lock-after-load request rejected, wrong cookie provided')
591 self.__pat.locked = True
592 self.__pat_lock_cookie = str(random.random())
593 return (1, self.__pat_lock_cookie)
594
596 if not self.__attached:
597 return (0, _('request rejected, you are not attach()ed'))
598 if auth_cookie != self.__auth_cookie:
599 _log.error('non-authenticated lock_into_patient()')
600 return (0, _('rejected lock_into_patient(), not authenticated'))
601 if self.__pat.locked:
602 _log.error('patient is already locked')
603 return (0, _('already locked into a patient'))
604 searcher = gmPerson.cPatientSearcher_SQL()
605 if type(search_params) == types.DictType:
606 idents = searcher.get_identities(search_dict=search_params)
607 print "must use dto, not search_dict"
608 print xxxxxxxxxxxxxxxxx
609 else:
610 idents = searcher.get_identities(search_term=search_params)
611 if idents is None:
612 return (0, _('error searching for patient with [%s]/%s') % (search_term, search_dict))
613 if len(idents) == 0:
614 return (0, _('no patient found for [%s]/%s') % (search_term, search_dict))
615
616 if len(idents) > 1:
617 return (0, _('several matching patients found for [%s]/%s') % (search_term, search_dict))
618 if not gmPatSearchWidgets.set_active_patient(patient = idents[0]):
619 return (0, _('cannot activate patient [%s] (%s/%s)') % (str(idents[0]), search_term, search_dict))
620 self.__pat.locked = True
621 self.__pat_lock_cookie = str(random.random())
622 return (1, self.__pat_lock_cookie)
623
625 if not self.__attached:
626 return (0, _('request rejected, you are not attach()ed'))
627 if auth_cookie != self.__auth_cookie:
628 _log.error('non-authenticated unlock_patient()')
629 return (0, _('rejected unlock_patient, not authenticated'))
630
631 if not self.__pat.locked:
632 return (1, '')
633
634 if unlock_cookie != self.__pat_lock_cookie:
635 _log.warning('patient unlock request rejected due to wrong cookie [%s]' % unlock_cookie)
636 return (0, 'patient unlock request rejected, wrong cookie provided')
637 self.__pat.locked = False
638 return (1, '')
639
641 if not self.__attached:
642 return 0
643 if auth_cookie != self.__auth_cookie:
644 _log.error('non-authenticated select_identity()')
645 return 0
646 return "cMacroPrimitives.assume_staff_identity() not implemented"
647
649 if not self.__user_done:
650 return (0, 'still waiting')
651 self.__user_done = False
652 return (1, self.__user_answer)
653
654
655
657 msg = _(
658 'Someone tries to forcibly break the existing\n'
659 'controlling connection. This may or may not\n'
660 'have legitimate reasons.\n\n'
661 'Do you want to allow breaking the connection ?'
662 )
663 can_break_conn = gmGuiHelpers.gm_show_question (
664 aMessage = msg,
665 aTitle = _('forced detach attempt')
666 )
667 if can_break_conn:
668 self.__user_answer = 1
669 else:
670 self.__user_answer = 0
671 self.__user_done = True
672 if can_break_conn:
673 self.__pat.locked = False
674 self.__attached = 0
675 return 1
676
678 top_win = wx.GetApp().GetTopWindow()
679 if forced:
680 top_win.Destroy()
681 else:
682 top_win.Close()
683
692
693
694
695 if __name__ == '__main__':
696
697 if len(sys.argv) < 2:
698 sys.exit()
699
700 if sys.argv[1] != 'test':
701 sys.exit()
702
703 gmI18N.activate_locale()
704 gmI18N.install_domain()
705
706
708 handler = gmPlaceholderHandler()
709 handler.debug = True
710
711 for placeholder in ['a', 'b']:
712 print handler[placeholder]
713
714 pat = gmPerson.ask_for_patient()
715 if pat is None:
716 return
717
718 gmPatSearchWidgets.set_active_patient(patient = pat)
719
720 print 'DOB (YYYY-MM-DD):', handler['date_of_birth::%Y-%m-%d']
721
722 app = wx.PyWidgetTester(size = (200, 50))
723 for placeholder in known_placeholders:
724 print placeholder, "=", handler[placeholder]
725
726 ph = 'progress_notes::ap'
727 print '%s: %s' % (ph, handler[ph])
728
730
731 tests = [
732
733 '$<lastname>$',
734 '$<lastname::::3>$',
735 '$<name::%(title)s %(firstnames)s%(preferred)s%(lastnames)s>$',
736
737
738 'lastname',
739 '$<lastname',
740 '$<lastname::',
741 '$<lastname::>$',
742 '$<lastname::abc>$',
743 '$<lastname::abc::>$',
744 '$<lastname::abc::3>$',
745 '$<lastname::abc::xyz>$',
746 '$<lastname::::>$',
747 '$<lastname::::xyz>$',
748
749 '$<date_of_birth::%Y-%m-%d>$',
750 '$<date_of_birth::%Y-%m-%d::3>$',
751 '$<date_of_birth::%Y-%m-%d::>$',
752
753
754 '$<adr_location::home::35>$',
755 '$<gender_mapper::male//female//other::5>$',
756 '$<current_meds::==> %(brand)s %(preparation)s (%(substance)s) <==\n::50>$',
757 '$<allergy_list::%(descriptor)s, >$',
758 '$<current_meds_table::latex//by-brand>$'
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773 ]
774
775 pat = gmPerson.ask_for_patient()
776 if pat is None:
777 return
778
779 gmPatSearchWidgets.set_active_patient(patient = pat)
780
781 handler = gmPlaceholderHandler()
782 handler.debug = True
783
784 for placeholder in tests:
785 print placeholder, "=>", handler[placeholder]
786 print "--------------"
787 raw_input()
788
789
790
791
792
793
794
795
796
797
798
800 from Gnumed.pycommon import gmScriptingListener
801 import xmlrpclib
802 listener = gmScriptingListener.cScriptingListener(macro_executor = cMacroPrimitives(personality='unit test'), port=9999)
803
804 s = xmlrpclib.ServerProxy('http://localhost:9999')
805 print "should fail:", s.attach()
806 print "should fail:", s.attach('wrong cookie')
807 print "should work:", s.version()
808 print "should fail:", s.raise_gnumed()
809 print "should fail:", s.raise_notebook_plugin('test plugin')
810 print "should fail:", s.lock_into_patient('kirk, james')
811 print "should fail:", s.unlock_patient()
812 status, conn_auth = s.attach('unit test')
813 print "should work:", status, conn_auth
814 print "should work:", s.version()
815 print "should work:", s.raise_gnumed(conn_auth)
816 status, pat_auth = s.lock_into_patient(conn_auth, 'kirk, james')
817 print "should work:", status, pat_auth
818 print "should fail:", s.unlock_patient(conn_auth, 'bogus patient unlock cookie')
819 print "should work", s.unlock_patient(conn_auth, pat_auth)
820 data = {'firstname': 'jame', 'lastnames': 'Kirk', 'gender': 'm'}
821 status, pat_auth = s.lock_into_patient(conn_auth, data)
822 print "should work:", status, pat_auth
823 print "should work", s.unlock_patient(conn_auth, pat_auth)
824 print s.detach('bogus detach cookie')
825 print s.detach(conn_auth)
826 del s
827
828 listener.shutdown()
829
831
832 import re as regex
833
834 tests = [
835 ' $<lastname>$ ',
836 ' $<lastname::::3>$ ',
837
838
839 '$<date_of_birth::%Y-%m-%d>$',
840 '$<date_of_birth::%Y-%m-%d::3>$',
841 '$<date_of_birth::%Y-%m-%d::>$',
842
843 '$<adr_location::home::35>$',
844 '$<gender_mapper::male//female//other::5>$',
845 '$<current_meds::==> %(brand)s %(preparation)s (%(substance)s) <==\\n::50>$',
846 '$<allergy_list::%(descriptor)s, >$',
847
848 '\\noindent Patient: $<lastname>$, $<firstname>$',
849 '$<allergies::%(descriptor)s & %(l10n_type)s & {\\footnotesize %(reaction)s} \tabularnewline \hline >$',
850 '$<current_meds:: \item[%(substance)s] {\\footnotesize (%(brand)s)} %(preparation)s %(strength)s: %(schedule)s >$'
851 ]
852
853 tests = [
854
855 'junk $<lastname::::3>$ junk',
856 'junk $<lastname::abc::3>$ junk',
857 'junk $<lastname::abc>$ junk',
858 'junk $<lastname>$ junk',
859
860 'junk $<lastname>$ junk $<firstname>$ junk',
861 'junk $<lastname::abc>$ junk $<fiststname::abc>$ junk',
862 'junk $<lastname::abc::3>$ junk $<firstname::abc::3>$ junk',
863 'junk $<lastname::::3>$ junk $<firstname::::3>$ junk'
864
865 ]
866
867 print "testing placeholder regex:", default_placeholder_regex
868 print ""
869
870 for t in tests:
871 print 'line: "%s"' % t
872 print "placeholders:"
873 for p in regex.findall(default_placeholder_regex, t, regex.IGNORECASE):
874 print ' => "%s"' % p
875 print " "
876
877
878
879 test_new_variant_placeholders()
880
881
882
883
884