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