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