Package Gnumed :: Package wxpython :: Module gmMacro
[frames] | no frames]

Source Code for Module Gnumed.wxpython.gmMacro

   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 
  18  if __name__ == '__main__': 
  19          gmI18N.activate_locale() 
  20          gmI18N.install_domain() 
  21  from Gnumed.pycommon import gmGuiBroker 
  22  from Gnumed.pycommon import gmTools 
  23  from Gnumed.pycommon import gmBorg 
  24  from Gnumed.pycommon import gmExceptions 
  25  from Gnumed.pycommon import gmCfg2 
  26  from Gnumed.pycommon import gmDateTime 
  27   
  28  from Gnumed.business import gmPerson 
  29  from Gnumed.business import gmDemographicRecord 
  30  from Gnumed.business import gmMedication 
  31  from Gnumed.business import gmPathLab 
  32  from Gnumed.business import gmPersonSearch 
  33  from Gnumed.business import gmVaccination 
  34  from Gnumed.business import gmPersonSearch 
  35   
  36  from Gnumed.wxpython import gmGuiHelpers 
  37  from Gnumed.wxpython import gmNarrativeWidgets 
  38  from Gnumed.wxpython import gmPatSearchWidgets 
  39  from Gnumed.wxpython import gmPlugin 
  40  from Gnumed.wxpython import gmEMRStructWidgets 
  41   
  42   
  43  _log = logging.getLogger('gm.scripting') 
  44  _cfg = gmCfg2.gmCfgData() 
  45   
  46  #===================================================================== 
  47  known_placeholders = [ 
  48          'lastname', 
  49          'firstname', 
  50          'title', 
  51          'date_of_birth', 
  52          'progress_notes', 
  53          'soap', 
  54          'soap_s', 
  55          'soap_o', 
  56          'soap_a', 
  57          'soap_p', 
  58          u'client_version', 
  59          u'current_provider', 
  60          u'allergy_state' 
  61  ] 
  62   
  63   
  64  # those must satisfy the pattern "$<name::args::optional length>$" when used 
  65  known_variant_placeholders = [ 
  66          u'soap', 
  67          u'progress_notes',                      # "args" holds: categories//template 
  68                                                                  #       categories: string with 'soap '; ' ' == None == admin 
  69                                                                  #       template:       u'something %s something'               (do not include // in template !) 
  70          u'emr_journal',                         # "args" format:   <categories>//<template>//<line length>//<time range>//<target format> 
  71                                                                  #       categories:        string with any of "s", "o", "a", "p", " "; 
  72                                                                  #                                  (" " == None == admin category) 
  73                                                                  #       template:          something %s something else 
  74                                                                  #                                  (Do not include // in the template !) 
  75                                                                  #       line length:   the length of individual lines, not the total placeholder length 
  76                                                                  #       time range:        the number of weeks going back in time 
  77                                                                  #       target format: "tex" or anything else, if "tex", data will be tex-escaped 
  78          u'date_of_birth', 
  79          u'adr_street',                          # "args" holds: type of address 
  80          u'adr_number', 
  81          u'adr_location', 
  82          u'adr_postcode', 
  83          u'gender_mapper',                       # "args" holds: value for male // value for female 
  84          u'current_meds',                        # "args" holds: line template 
  85          u'current_meds_table',          # "args" holds: format, options 
  86          u'current_meds_notes',          # "args" holds: format, options 
  87          u'lab_table',                           # "args" holds: format (currently "latex" only) 
  88          u'latest_vaccs_table',          # "args" holds: format, options 
  89          u'today',                                       # "args" holds: strftime format 
  90          u'tex_escape',                          # "args" holds: string to escape 
  91          u'allergies',                           # "args" holds: line template, one allergy per line 
  92          u'allergy_list',                        # "args" holds: template per allergy, allergies on one line 
  93          u'problems',                            # "args" holds: line template, one problem per line 
  94          u'name',                                        # "args" holds: template for name parts arrangement 
  95          u'free_text',                           # show a dialog for entering some free text 
  96          u'soap_for_encounters',         # "args" holds: soap cats // strftime date format 
  97          u'encounter_list'                       # "args" holds: per-encounter template, each ends up on one line 
  98  ] 
  99   
 100  default_placeholder_regex = r'\$<.+?>\$'                                # this one works (except that OOo cannot be non-greedy |-( ) 
 101   
 102  #_regex_parts = [ 
 103  #       r'\$<\w+::.*(?::)\d+>\$', 
 104  #       r'\$<\w+::.+(?!>\$)>\$', 
 105  #       r'\$<\w+?>\$' 
 106  #] 
 107  #default_placeholder_regex = r'|'.join(_regex_parts) 
 108   
 109  default_placeholder_start = u'$<' 
 110  default_placeholder_end = u'>$' 
 111  #===================================================================== 
112 -class gmPlaceholderHandler(gmBorg.cBorg):
113 """Replaces placeholders in forms, fields, etc. 114 115 - patient related placeholders operate on the currently active patient 116 - is passed to the forms handling code, for example 117 118 Note that this cannot be called from a non-gui thread unless 119 wrapped in wx.CallAfter. 120 121 There are currently three types of placeholders: 122 123 simple static placeholders 124 - those are listed in known_placeholders 125 - they are used as-is 126 127 extended static placeholders 128 - those are like the static ones but have "::::<NUMBER>" appended 129 where <NUMBER> is the maximum length 130 131 variant placeholders 132 - those are listed in known_variant_placeholders 133 - they are parsed into placeholder, data, and maximum length 134 - the length is optional 135 - data is passed to the handler 136 """
137 - def __init__(self, *args, **kwargs):
138 139 self.pat = gmPerson.gmCurrentPatient() 140 self.debug = False 141 142 self.invalid_placeholder_template = _('invalid placeholder [%s]')
143 #-------------------------------------------------------- 144 # __getitem__ API 145 #--------------------------------------------------------
146 - def __getitem__(self, placeholder):
147 """Map self['placeholder'] to self.placeholder. 148 149 This is useful for replacing placeholders parsed out 150 of documents as strings. 151 152 Unknown/invalid placeholders still deliver a result but 153 it will be glaringly obvious if debugging is enabled. 154 """ 155 _log.debug('replacing [%s]', placeholder) 156 157 original_placeholder = placeholder 158 159 if placeholder.startswith(default_placeholder_start): 160 placeholder = placeholder[len(default_placeholder_start):] 161 if placeholder.endswith(default_placeholder_end): 162 placeholder = placeholder[:-len(default_placeholder_end)] 163 else: 164 _log.debug('placeholder must either start with [%s] and end with [%s] or neither of both', default_placeholder_start, default_placeholder_end) 165 if self.debug: 166 return self.invalid_placeholder_template % original_placeholder 167 return None 168 169 # simple static placeholder ? 170 if placeholder in known_placeholders: 171 return getattr(self, placeholder) 172 173 # extended static placeholder ? 174 parts = placeholder.split('::::', 1) 175 if len(parts) == 2: 176 name, lng = parts 177 try: 178 return getattr(self, name)[:int(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 # variable placeholders 186 parts = placeholder.split('::') 187 if len(parts) == 2: 188 name, data = parts 189 lng = None 190 if len(parts) == 3: 191 name, data, lng = parts 192 try: 193 lng = int(lng) 194 except (TypeError, ValueError): 195 _log.error('placeholder length definition error: %s, discarding length: >%s<', original_placeholder, lng) 196 lng = None 197 if len(parts) > 3: 198 _log.warning('invalid placeholder layout: %s', original_placeholder) 199 if self.debug: 200 return self.invalid_placeholder_template % original_placeholder 201 return None 202 203 handler = getattr(self, '_get_variant_%s' % name, None) 204 if handler is None: 205 _log.warning('no handler <_get_variant_%s> for placeholder %s', name, original_placeholder) 206 if self.debug: 207 return self.invalid_placeholder_template % original_placeholder 208 return None 209 210 try: 211 if lng is None: 212 return handler(data = data) 213 return handler(data = data)[:lng] 214 except: 215 _log.exception('placeholder handling error: %s', original_placeholder) 216 if self.debug: 217 return self.invalid_placeholder_template % original_placeholder 218 return None 219 220 _log.error('something went wrong, should never get here') 221 return None
222 #-------------------------------------------------------- 223 # properties actually handling placeholders 224 #-------------------------------------------------------- 225 # property helpers 226 #--------------------------------------------------------
227 - def _setter_noop(self, val):
228 """This does nothing, used as a NOOP properties setter.""" 229 pass
230 #--------------------------------------------------------
231 - def _get_lastname(self):
232 return self.pat.get_active_name()['lastnames']
233 #--------------------------------------------------------
234 - def _get_firstname(self):
235 return self.pat.get_active_name()['firstnames']
236 #--------------------------------------------------------
237 - def _get_title(self):
238 return gmTools.coalesce(self.pat.get_active_name()['title'], u'')
239 #--------------------------------------------------------
240 - def _get_dob(self):
241 return self._get_variant_date_of_birth(data='%x')
242 #--------------------------------------------------------
243 - def _get_progress_notes(self):
244 return self._get_variant_soap()
245 #--------------------------------------------------------
246 - def _get_soap_s(self):
247 return self._get_variant_soap(data = u's')
248 #--------------------------------------------------------
249 - def _get_soap_o(self):
250 return self._get_variant_soap(data = u'o')
251 #--------------------------------------------------------
252 - def _get_soap_a(self):
253 return self._get_variant_soap(data = u'a')
254 #--------------------------------------------------------
255 - def _get_soap_p(self):
256 return self._get_variant_soap(data = u'p')
257 #--------------------------------------------------------
258 - def _get_soap_admin(self):
259 return self._get_variant_soap(soap_cats = None)
260 #--------------------------------------------------------
261 - def _get_client_version(self):
262 return gmTools.coalesce ( 263 _cfg.get(option = u'client_version'), 264 u'%s' % self.__class__.__name__ 265 )
266 #--------------------------------------------------------
267 - def _get_current_provider(self):
268 prov = gmPerson.gmCurrentProvider() 269 270 title = gmTools.coalesce ( 271 prov['title'], 272 gmPerson.map_gender2salutation(prov['gender']) 273 ) 274 275 tmp = u'%s %s. %s' % ( 276 title, 277 prov['firstnames'][:1], 278 prov['lastnames'] 279 ) 280 281 return tmp
282 #--------------------------------------------------------
283 - def _get_allergy_state(self):
284 allg_state = self.pat.get_emr().allergy_state 285 286 if allg_state['last_confirmed'] is None: 287 date_confirmed = u'' 288 else: 289 date_confirmed = u' (%s)' % allg_state['last_confirmed'].strftime('%Y %B %d').decode(gmI18N.get_encoding()) 290 291 tmp = u'%s%s' % ( 292 allg_state.state_string, 293 date_confirmed 294 ) 295 return tmp
296 #-------------------------------------------------------- 297 # property definitions for static placeholders 298 #-------------------------------------------------------- 299 placeholder_regex = property(lambda x: default_placeholder_regex, _setter_noop) 300 301 # placeholders 302 lastname = property(_get_lastname, _setter_noop) 303 firstname = property(_get_firstname, _setter_noop) 304 title = property(_get_title, _setter_noop) 305 date_of_birth = property(_get_dob, _setter_noop) 306 307 progress_notes = property(_get_progress_notes, _setter_noop) 308 soap = property(_get_progress_notes, _setter_noop) 309 soap_s = property(_get_soap_s, _setter_noop) 310 soap_o = property(_get_soap_o, _setter_noop) 311 soap_a = property(_get_soap_a, _setter_noop) 312 soap_p = property(_get_soap_p, _setter_noop) 313 soap_admin = property(_get_soap_admin, _setter_noop) 314 315 allergy_state = property(_get_allergy_state, _setter_noop) 316 317 client_version = property(_get_client_version, _setter_noop) 318 319 current_provider = property(_get_current_provider, _setter_noop) 320 #-------------------------------------------------------- 321 # variant handlers 322 #--------------------------------------------------------
323 - def _get_variant_encounter_list(self, data=None):
324 325 encounters = gmEMRStructWidgets.select_encounters(single_selection = False) 326 if not encounters: 327 return u'' 328 329 template = data 330 331 lines = [] 332 for enc in encounters: 333 try: 334 lines.append(template % enc) 335 except: 336 lines.append(u'error formatting encounter') 337 _log.exception('problem formatting encounter list') 338 _log.error('template: %s', template) 339 _log.error('encounter: %s', encounter) 340 341 return u'\n'.join(lines)
342 #--------------------------------------------------------
343 - def _get_variant_soap_for_encounters(self, data=None):
344 """Select encounters from list and format SOAP thereof. 345 346 data: soap_cats (' ' -> None -> admin) // date format 347 """ 348 # defaults 349 cats = None 350 date_format = None 351 352 if data is not None: 353 data_parts = data.split('//') 354 355 # part[0]: categories 356 if len(data_parts[0]) > 0: 357 cats = [] 358 if u' ' in data_parts[0]: 359 cats.append(None) 360 data_parts[0] = data_parts[0].replace(u' ', u'') 361 cats.extend(list(data_parts[0])) 362 363 # part[1]: date format 364 if len(data_parts) > 1: 365 if len(data_parts[1]) > 0: 366 date_format = data_parts[1] 367 368 encounters = gmEMRStructWidgets.select_encounters(single_selection = False) 369 if not encounters: 370 return u'' 371 372 chunks = [] 373 for enc in encounters: 374 chunks.append(enc.format_latex ( 375 date_format = date_format, 376 soap_cats = cats, 377 soap_order = u'soap_rank, date' 378 )) 379 380 return u''.join(chunks)
381 #--------------------------------------------------------
382 - def _get_variant_emr_journal(self, data=None):
383 # default: all categories, neutral template 384 cats = list(u'soap') 385 cats.append(None) 386 template = u'%s' 387 interactive = True 388 line_length = 9999 389 target_format = None 390 time_range = None 391 392 if data is not None: 393 data_parts = data.split('//') 394 395 # part[0]: categories 396 cats = [] 397 # ' ' -> None == admin 398 for c in list(data_parts[0]): 399 if c == u' ': 400 c = None 401 cats.append(c) 402 # '' -> SOAP + None 403 if cats == u'': 404 cats = list(u'soap').append(None) 405 406 # part[1]: template 407 if len(data_parts) > 1: 408 template = data_parts[1] 409 410 # part[2]: line length 411 if len(data_parts) > 2: 412 try: 413 line_length = int(data_parts[2]) 414 except: 415 line_length = 9999 416 417 # part[3]: weeks going back in time 418 if len(data_parts) > 3: 419 try: 420 time_range = 7 * int(data_parts[3]) 421 except: 422 time_range = None 423 424 # part[4]: output format 425 if len(data_parts) > 4: 426 target_format = data_parts[4] 427 428 # FIXME: will need to be a generator later on 429 narr = self.pat.get_emr().get_as_journal(soap_cats = cats, time_range = time_range) 430 431 if len(narr) == 0: 432 return u'' 433 434 if target_format == u'tex': 435 keys = narr[0].keys() 436 lines = [] 437 line_dict = {} 438 for n in narr: 439 for key in keys: 440 if isinstance(n[key], basestring): 441 line_dict[key] = gmTools.tex_escape_string(text = n[key]) 442 continue 443 line_dict[key] = n[key] 444 try: 445 lines.append((template % line_dict)[:line_length]) 446 except KeyError: 447 return u'invalid key in template [%s], valid keys: %s]' % (template, str(keys)) 448 else: 449 try: 450 lines = [ (template % n)[:line_length] for n in narr ] 451 except KeyError: 452 return u'invalid key in template [%s], valid keys: %s]' % (template, str(narr[0].keys())) 453 454 return u'\n'.join(lines)
455 #--------------------------------------------------------
456 - def _get_variant_progress_notes(self, data=None):
457 return self._get_variant_soap(data=data)
458 #--------------------------------------------------------
459 - def _get_variant_soap(self, data=None):
460 461 # default: all categories, neutral template 462 cats = list(u'soap') 463 cats.append(None) 464 template = u'%s' 465 466 if data is not None: 467 data_parts = data.split('//') 468 469 # part[0]: categories 470 cats = [] 471 # ' ' -> None == admin 472 for cat in list(data_parts[0]): 473 if cat == u' ': 474 cat = None 475 cats.append(cat) 476 # '' -> SOAP + None 477 if cats == u'': 478 cats = list(u'soap') 479 cats.append(None) 480 481 # part[1]: template 482 if len(data_parts) > 1: 483 template = data_parts[1] 484 485 #narr = gmNarrativeWidgets.select_narrative_from_episodes_new(soap_cats = cats) 486 narr = gmNarrativeWidgets.select_narrative_from_episodes(soap_cats = cats) 487 488 if narr is None: 489 return u'' 490 491 if len(narr) == 0: 492 return u'' 493 494 try: 495 narr = [ template % n['narrative'] for n in narr ] 496 except KeyError: 497 return u'invalid key in template [%s], valid keys: %s]' % (template, str(narr[0].keys())) 498 499 return u'\n'.join(narr)
500 #--------------------------------------------------------
501 - def _get_variant_name(self, data=None):
502 if data is None: 503 return [_('template is missing')] 504 505 name = self.pat.get_active_name() 506 507 parts = { 508 'title': gmTools.coalesce(name['title'], u''), 509 'firstnames': name['firstnames'], 510 'lastnames': name['lastnames'], 511 'preferred': gmTools.coalesce ( 512 initial = name['preferred'], 513 instead = u' ', 514 template_initial = u' "%s" ' 515 ) 516 } 517 518 return data % parts
519 #--------------------------------------------------------
520 - def _get_variant_date_of_birth(self, data='%x'):
521 return self.pat.get_formatted_dob(format = str(data), encoding = gmI18N.get_encoding())
522 #-------------------------------------------------------- 523 # FIXME: extend to all supported genders
524 - def _get_variant_gender_mapper(self, data='male//female//other'):
525 values = data.split('//', 2) 526 527 if len(values) == 2: 528 male_value, female_value = values 529 other_value = u'<unkown gender>' 530 elif len(values) == 3: 531 male_value, female_value, other_value = values 532 else: 533 return _('invalid gender mapping layout: [%s]') % data 534 535 if self.pat['gender'] == u'm': 536 return male_value 537 538 if self.pat['gender'] == u'f': 539 return female_value 540 541 return other_value
542 #--------------------------------------------------------
543 - def _get_variant_adr_street(self, data=u'?'):
544 # if data == u'?': 545 # types = xxxxxxxxxxx 546 adrs = self.pat.get_addresses(address_type=data) 547 if len(adrs) == 0: 548 return _('no street for address type [%s]') % data 549 return adrs[0]['street']
550 #--------------------------------------------------------
551 - def _get_variant_adr_number(self, data=u'?'):
552 adrs = self.pat.get_addresses(address_type=data) 553 if len(adrs) == 0: 554 return _('no number for address type [%s]') % data 555 return adrs[0]['number']
556 #--------------------------------------------------------
557 - def _get_variant_adr_location(self, data=u'?'):
558 adrs = self.pat.get_addresses(address_type=data) 559 if len(adrs) == 0: 560 return _('no location for address type [%s]') % data 561 return adrs[0]['urb']
562 #--------------------------------------------------------
563 - def _get_variant_adr_postcode(self, data=u'?'):
564 adrs = self.pat.get_addresses(address_type=data) 565 if len(adrs) == 0: 566 return _('no postcode for address type [%s]') % data 567 return adrs[0]['postcode']
568 #--------------------------------------------------------
569 - def _get_variant_allergy_list(self, data=None):
570 if data is None: 571 return [_('template is missing')] 572 573 template, separator = data.split('//', 2) 574 575 emr = self.pat.get_emr() 576 return separator.join([ template % a for a in emr.get_allergies() ])
577 #--------------------------------------------------------
578 - def _get_variant_allergies(self, data=None):
579 580 if data is None: 581 return [_('template is missing')] 582 583 emr = self.pat.get_emr() 584 return u'\n'.join([ data % a for a in emr.get_allergies() ])
585 #--------------------------------------------------------
586 - def _get_variant_current_meds(self, data=None):
587 588 if data is None: 589 return [_('template is missing')] 590 591 emr = self.pat.get_emr() 592 current_meds = emr.get_current_substance_intake ( 593 include_inactive = False, 594 include_unapproved = False, 595 order_by = u'brand, substance' 596 ) 597 598 # FIXME: we should be dealing with translating None to u'' here 599 600 return u'\n'.join([ data % m for m in current_meds ])
601 #--------------------------------------------------------
602 - def _get_variant_current_meds_table(self, data=None):
603 604 options = data.split('//') 605 606 if u'latex' in options: 607 return gmMedication.format_substance_intake ( 608 emr = self.pat.get_emr(), 609 output_format = u'latex', 610 table_type = u'by-brand' 611 ) 612 613 _log.error('no known current medications table formatting style in [%]', data) 614 return _('unknown current medication table formatting style')
615 #--------------------------------------------------------
616 - def _get_variant_current_meds_notes(self, data=None):
617 618 options = data.split('//') 619 620 if u'latex' in options: 621 return gmMedication.format_substance_intake_notes ( 622 emr = self.pat.get_emr(), 623 output_format = u'latex', 624 table_type = u'by-brand' 625 ) 626 627 _log.error('no known current medications notes formatting style in [%]', data) 628 return _('unknown current medication notes formatting style')
629 #--------------------------------------------------------
630 - def _get_variant_lab_table(self, data=None):
631 632 options = data.split('//') 633 634 emr = self.pat.get_emr() 635 636 if u'latex' in options: 637 return gmPathLab.format_test_results ( 638 results = emr.get_test_results_by_date(), 639 output_format = u'latex' 640 ) 641 642 _log.error('no known test results table formatting style in [%s]', data) 643 return _('unknown test results table formatting style [%s]') % data
644 #--------------------------------------------------------
645 - def _get_variant_latest_vaccs_table(self, data=None):
646 647 options = data.split('//') 648 649 emr = self.pat.get_emr() 650 651 if u'latex' in options: 652 return gmVaccination.format_latest_vaccinations(output_format = u'latex', emr = emr) 653 654 _log.error('no known vaccinations table formatting style in [%s]', data) 655 return _('unknown vaccinations table formatting style [%s]') % data
656 #--------------------------------------------------------
657 - def _get_variant_problems(self, data=None):
658 659 if data is None: 660 return [_('template is missing')] 661 662 probs = self.pat.get_emr().get_problems() 663 664 return u'\n'.join([ data % p for p in probs ])
665 #--------------------------------------------------------
666 - def _get_variant_today(self, data='%x'):
667 return gmDateTime.pydt_now_here().strftime(str(data)).decode(gmI18N.get_encoding())
668 #--------------------------------------------------------
669 - def _get_variant_tex_escape(self, data=None):
670 return gmTools.tex_escape_string(text = data)
671 #--------------------------------------------------------
672 - def _get_variant_free_text(self, data=u'tex//'):
673 # <data>: 674 # format: tex (only, currently) 675 # message: shown in input dialog, must not contain "//" or "::" 676 677 data_parts = data.split('//') 678 format = data_parts[0] 679 if len(data_parts) > 1: 680 msg = data_parts[1] 681 else: 682 msg = _('generic text') 683 684 dlg = gmGuiHelpers.cMultilineTextEntryDlg ( 685 None, 686 -1, 687 title = _('Replacing <free_text> placeholder'), 688 msg = _('Below you can enter free text.\n\n [%s]') % msg 689 ) 690 dlg.enable_user_formatting = True 691 decision = dlg.ShowModal() 692 693 if decision != wx.ID_SAVE: 694 dlg.Destroy() 695 return _('Text input cancelled by user.') 696 697 text = dlg.value.strip() 698 if dlg.is_user_formatted: 699 dlg.Destroy() 700 return text 701 702 dlg.Destroy() 703 704 if format == u'tex': 705 return gmTools.tex_escape_string(text = text) 706 707 return text
708 #-------------------------------------------------------- 709 # internal helpers 710 #-------------------------------------------------------- 711 712 #=====================================================================
713 -class cMacroPrimitives:
714 """Functions a macro can legally use. 715 716 An instance of this class is passed to the GNUmed scripting 717 listener. Hence, all actions a macro can legally take must 718 be defined in this class. Thus we achieve some screening for 719 security and also thread safety handling. 720 """ 721 #-----------------------------------------------------------------
722 - def __init__(self, personality = None):
723 if personality is None: 724 raise gmExceptions.ConstructorError, 'must specify personality' 725 self.__personality = personality 726 self.__attached = 0 727 self._get_source_personality = None 728 self.__user_done = False 729 self.__user_answer = 'no answer yet' 730 self.__pat = gmPerson.gmCurrentPatient() 731 732 self.__auth_cookie = str(random.random()) 733 self.__pat_lock_cookie = str(random.random()) 734 self.__lock_after_load_cookie = str(random.random()) 735 736 _log.info('slave mode personality is [%s]', personality)
737 #----------------------------------------------------------------- 738 # public API 739 #-----------------------------------------------------------------
740 - def attach(self, personality = None):
741 if self.__attached: 742 _log.error('attach with [%s] rejected, already serving a client', personality) 743 return (0, _('attach rejected, already serving a client')) 744 if personality != self.__personality: 745 _log.error('rejecting attach to personality [%s], only servicing [%s]' % (personality, self.__personality)) 746 return (0, _('attach to personality [%s] rejected') % personality) 747 self.__attached = 1 748 self.__auth_cookie = str(random.random()) 749 return (1, self.__auth_cookie)
750 #-----------------------------------------------------------------
751 - def detach(self, auth_cookie=None):
752 if not self.__attached: 753 return 1 754 if auth_cookie != self.__auth_cookie: 755 _log.error('rejecting detach() with cookie [%s]' % auth_cookie) 756 return 0 757 self.__attached = 0 758 return 1
759 #-----------------------------------------------------------------
760 - def force_detach(self):
761 if not self.__attached: 762 return 1 763 self.__user_done = False 764 # FIXME: use self.__sync_cookie for syncing with user interaction 765 wx.CallAfter(self._force_detach) 766 return 1
767 #-----------------------------------------------------------------
768 - def version(self):
769 ver = _cfg.get(option = u'client_version') 770 return "GNUmed %s, %s $Revision: 1.51 $" % (ver, self.__class__.__name__)
771 #-----------------------------------------------------------------
772 - def shutdown_gnumed(self, auth_cookie=None, forced=False):
773 """Shuts down this client instance.""" 774 if not self.__attached: 775 return 0 776 if auth_cookie != self.__auth_cookie: 777 _log.error('non-authenticated shutdown_gnumed()') 778 return 0 779 wx.CallAfter(self._shutdown_gnumed, forced) 780 return 1
781 #-----------------------------------------------------------------
782 - def raise_gnumed(self, auth_cookie = None):
783 """Raise ourselves to the top of the desktop.""" 784 if not self.__attached: 785 return 0 786 if auth_cookie != self.__auth_cookie: 787 _log.error('non-authenticated raise_gnumed()') 788 return 0 789 return "cMacroPrimitives.raise_gnumed() not implemented"
790 #-----------------------------------------------------------------
791 - def get_loaded_plugins(self, auth_cookie = None):
792 if not self.__attached: 793 return 0 794 if auth_cookie != self.__auth_cookie: 795 _log.error('non-authenticated get_loaded_plugins()') 796 return 0 797 gb = gmGuiBroker.GuiBroker() 798 return gb['horstspace.notebook.gui'].keys()
799 #-----------------------------------------------------------------
800 - def raise_notebook_plugin(self, auth_cookie = None, a_plugin = None):
801 """Raise a notebook plugin within GNUmed.""" 802 if not self.__attached: 803 return 0 804 if auth_cookie != self.__auth_cookie: 805 _log.error('non-authenticated raise_notebook_plugin()') 806 return 0 807 # FIXME: use semaphore 808 wx.CallAfter(gmPlugin.raise_notebook_plugin, a_plugin) 809 return 1
810 #-----------------------------------------------------------------
811 - def load_patient_from_external_source(self, auth_cookie = None):
812 """Load external patient, perhaps create it. 813 814 Callers must use get_user_answer() to get status information. 815 It is unsafe to proceed without knowing the completion state as 816 the controlled client may be waiting for user input from a 817 patient selection list. 818 """ 819 if not self.__attached: 820 return (0, _('request rejected, you are not attach()ed')) 821 if auth_cookie != self.__auth_cookie: 822 _log.error('non-authenticated load_patient_from_external_source()') 823 return (0, _('rejected load_patient_from_external_source(), not authenticated')) 824 if self.__pat.locked: 825 _log.error('patient is locked, cannot load from external source') 826 return (0, _('current patient is locked')) 827 self.__user_done = False 828 wx.CallAfter(self._load_patient_from_external_source) 829 self.__lock_after_load_cookie = str(random.random()) 830 return (1, self.__lock_after_load_cookie)
831 #-----------------------------------------------------------------
832 - def lock_loaded_patient(self, auth_cookie = None, lock_after_load_cookie = None):
833 if not self.__attached: 834 return (0, _('request rejected, you are not attach()ed')) 835 if auth_cookie != self.__auth_cookie: 836 _log.error('non-authenticated lock_load_patient()') 837 return (0, _('rejected lock_load_patient(), not authenticated')) 838 # FIXME: ask user what to do about wrong cookie 839 if lock_after_load_cookie != self.__lock_after_load_cookie: 840 _log.warning('patient lock-after-load request rejected due to wrong cookie [%s]' % lock_after_load_cookie) 841 return (0, 'patient lock-after-load request rejected, wrong cookie provided') 842 self.__pat.locked = True 843 self.__pat_lock_cookie = str(random.random()) 844 return (1, self.__pat_lock_cookie)
845 #-----------------------------------------------------------------
846 - def lock_into_patient(self, auth_cookie = None, search_params = None):
847 if not self.__attached: 848 return (0, _('request rejected, you are not attach()ed')) 849 if auth_cookie != self.__auth_cookie: 850 _log.error('non-authenticated lock_into_patient()') 851 return (0, _('rejected lock_into_patient(), not authenticated')) 852 if self.__pat.locked: 853 _log.error('patient is already locked') 854 return (0, _('already locked into a patient')) 855 searcher = gmPersonSearch.cPatientSearcher_SQL() 856 if type(search_params) == types.DictType: 857 idents = searcher.get_identities(search_dict=search_params) 858 print "must use dto, not search_dict" 859 print xxxxxxxxxxxxxxxxx 860 else: 861 idents = searcher.get_identities(search_term=search_params) 862 if idents is None: 863 return (0, _('error searching for patient with [%s]/%s') % (search_term, search_dict)) 864 if len(idents) == 0: 865 return (0, _('no patient found for [%s]/%s') % (search_term, search_dict)) 866 # FIXME: let user select patient 867 if len(idents) > 1: 868 return (0, _('several matching patients found for [%s]/%s') % (search_term, search_dict)) 869 if not gmPatSearchWidgets.set_active_patient(patient = idents[0]): 870 return (0, _('cannot activate patient [%s] (%s/%s)') % (str(idents[0]), search_term, search_dict)) 871 self.__pat.locked = True 872 self.__pat_lock_cookie = str(random.random()) 873 return (1, self.__pat_lock_cookie)
874 #-----------------------------------------------------------------
875 - def unlock_patient(self, auth_cookie = None, unlock_cookie = None):
876 if not self.__attached: 877 return (0, _('request rejected, you are not attach()ed')) 878 if auth_cookie != self.__auth_cookie: 879 _log.error('non-authenticated unlock_patient()') 880 return (0, _('rejected unlock_patient, not authenticated')) 881 # we ain't locked anyways, so succeed 882 if not self.__pat.locked: 883 return (1, '') 884 # FIXME: ask user what to do about wrong cookie 885 if unlock_cookie != self.__pat_lock_cookie: 886 _log.warning('patient unlock request rejected due to wrong cookie [%s]' % unlock_cookie) 887 return (0, 'patient unlock request rejected, wrong cookie provided') 888 self.__pat.locked = False 889 return (1, '')
890 #-----------------------------------------------------------------
891 - def assume_staff_identity(self, auth_cookie = None, staff_name = "Dr.Jekyll", staff_creds = None):
892 if not self.__attached: 893 return 0 894 if auth_cookie != self.__auth_cookie: 895 _log.error('non-authenticated select_identity()') 896 return 0 897 return "cMacroPrimitives.assume_staff_identity() not implemented"
898 #-----------------------------------------------------------------
899 - def get_user_answer(self):
900 if not self.__user_done: 901 return (0, 'still waiting') 902 self.__user_done = False 903 return (1, self.__user_answer)
904 #----------------------------------------------------------------- 905 # internal API 906 #-----------------------------------------------------------------
907 - def _force_detach(self):
908 msg = _( 909 'Someone tries to forcibly break the existing\n' 910 'controlling connection. This may or may not\n' 911 'have legitimate reasons.\n\n' 912 'Do you want to allow breaking the connection ?' 913 ) 914 can_break_conn = gmGuiHelpers.gm_show_question ( 915 aMessage = msg, 916 aTitle = _('forced detach attempt') 917 ) 918 if can_break_conn: 919 self.__user_answer = 1 920 else: 921 self.__user_answer = 0 922 self.__user_done = True 923 if can_break_conn: 924 self.__pat.locked = False 925 self.__attached = 0 926 return 1
927 #-----------------------------------------------------------------
928 - def _shutdown_gnumed(self, forced=False):
929 top_win = wx.GetApp().GetTopWindow() 930 if forced: 931 top_win.Destroy() 932 else: 933 top_win.Close()
934 #-----------------------------------------------------------------
936 patient = gmPatSearchWidgets.get_person_from_external_sources(search_immediately = True, activate_immediately = True) 937 if patient is not None: 938 self.__user_answer = 1 939 else: 940 self.__user_answer = 0 941 self.__user_done = True 942 return 1
943 #===================================================================== 944 # main 945 #===================================================================== 946 if __name__ == '__main__': 947 948 if len(sys.argv) < 2: 949 sys.exit() 950 951 if sys.argv[1] != 'test': 952 sys.exit() 953 954 gmI18N.activate_locale() 955 gmI18N.install_domain() 956 957 #--------------------------------------------------------
958 - def test_placeholders():
959 handler = gmPlaceholderHandler() 960 handler.debug = True 961 962 for placeholder in ['a', 'b']: 963 print handler[placeholder] 964 965 pat = gmPersonSearch.ask_for_patient() 966 if pat is None: 967 return 968 969 gmPatSearchWidgets.set_active_patient(patient = pat) 970 971 print 'DOB (YYYY-MM-DD):', handler['date_of_birth::%Y-%m-%d'] 972 973 app = wx.PyWidgetTester(size = (200, 50)) 974 for placeholder in known_placeholders: 975 print placeholder, "=", handler[placeholder] 976 977 ph = 'progress_notes::ap' 978 print '%s: %s' % (ph, handler[ph])
979 #--------------------------------------------------------
980 - def test_new_variant_placeholders():
981 982 tests = [ 983 # should work: 984 '$<lastname>$', 985 '$<lastname::::3>$', 986 '$<name::%(title)s %(firstnames)s%(preferred)s%(lastnames)s>$', 987 988 # should fail: 989 'lastname', 990 '$<lastname', 991 '$<lastname::', 992 '$<lastname::>$', 993 '$<lastname::abc>$', 994 '$<lastname::abc::>$', 995 '$<lastname::abc::3>$', 996 '$<lastname::abc::xyz>$', 997 '$<lastname::::>$', 998 '$<lastname::::xyz>$', 999 1000 '$<date_of_birth::%Y-%m-%d>$', 1001 '$<date_of_birth::%Y-%m-%d::3>$', 1002 '$<date_of_birth::%Y-%m-%d::>$', 1003 1004 # should work: 1005 '$<adr_location::home::35>$', 1006 '$<gender_mapper::male//female//other::5>$', 1007 '$<current_meds::==> %(brand)s %(preparation)s (%(substance)s) <==\n::50>$', 1008 '$<allergy_list::%(descriptor)s, >$', 1009 '$<current_meds_table::latex//by-brand>$' 1010 1011 # 'firstname', 1012 # 'title', 1013 # 'date_of_birth', 1014 # 'progress_notes', 1015 # 'soap', 1016 # 'soap_s', 1017 # 'soap_o', 1018 # 'soap_a', 1019 # 'soap_p', 1020 1021 # 'soap', 1022 # 'progress_notes', 1023 # 'date_of_birth' 1024 ] 1025 1026 tests = [ 1027 '$<latest_vaccs_table::latex>$' 1028 ] 1029 1030 pat = gmPersonSearch.ask_for_patient() 1031 if pat is None: 1032 return 1033 1034 gmPatSearchWidgets.set_active_patient(patient = pat) 1035 1036 handler = gmPlaceholderHandler() 1037 handler.debug = True 1038 1039 for placeholder in tests: 1040 print placeholder, "=>", handler[placeholder] 1041 print "--------------" 1042 raw_input()
1043 1044 # print 'DOB (YYYY-MM-DD):', handler['date_of_birth::%Y-%m-%d'] 1045 1046 # app = wx.PyWidgetTester(size = (200, 50)) 1047 # for placeholder in known_placeholders: 1048 # print placeholder, "=", handler[placeholder] 1049 1050 # ph = 'progress_notes::ap' 1051 # print '%s: %s' % (ph, handler[ph]) 1052 1053 #--------------------------------------------------------
1054 - def test_scripting():
1055 from Gnumed.pycommon import gmScriptingListener 1056 import xmlrpclib 1057 listener = gmScriptingListener.cScriptingListener(macro_executor = cMacroPrimitives(personality='unit test'), port=9999) 1058 1059 s = xmlrpclib.ServerProxy('http://localhost:9999') 1060 print "should fail:", s.attach() 1061 print "should fail:", s.attach('wrong cookie') 1062 print "should work:", s.version() 1063 print "should fail:", s.raise_gnumed() 1064 print "should fail:", s.raise_notebook_plugin('test plugin') 1065 print "should fail:", s.lock_into_patient('kirk, james') 1066 print "should fail:", s.unlock_patient() 1067 status, conn_auth = s.attach('unit test') 1068 print "should work:", status, conn_auth 1069 print "should work:", s.version() 1070 print "should work:", s.raise_gnumed(conn_auth) 1071 status, pat_auth = s.lock_into_patient(conn_auth, 'kirk, james') 1072 print "should work:", status, pat_auth 1073 print "should fail:", s.unlock_patient(conn_auth, 'bogus patient unlock cookie') 1074 print "should work", s.unlock_patient(conn_auth, pat_auth) 1075 data = {'firstname': 'jame', 'lastnames': 'Kirk', 'gender': 'm'} 1076 status, pat_auth = s.lock_into_patient(conn_auth, data) 1077 print "should work:", status, pat_auth 1078 print "should work", s.unlock_patient(conn_auth, pat_auth) 1079 print s.detach('bogus detach cookie') 1080 print s.detach(conn_auth) 1081 del s 1082 1083 listener.shutdown()
1084 #--------------------------------------------------------
1085 - def test_placeholder_regex():
1086 1087 import re as regex 1088 1089 tests = [ 1090 ' $<lastname>$ ', 1091 ' $<lastname::::3>$ ', 1092 1093 # should fail: 1094 '$<date_of_birth::%Y-%m-%d>$', 1095 '$<date_of_birth::%Y-%m-%d::3>$', 1096 '$<date_of_birth::%Y-%m-%d::>$', 1097 1098 '$<adr_location::home::35>$', 1099 '$<gender_mapper::male//female//other::5>$', 1100 '$<current_meds::==> %(brand)s %(preparation)s (%(substance)s) <==\\n::50>$', 1101 '$<allergy_list::%(descriptor)s, >$', 1102 1103 '\\noindent Patient: $<lastname>$, $<firstname>$', 1104 '$<allergies::%(descriptor)s & %(l10n_type)s & {\\footnotesize %(reaction)s} \tabularnewline \hline >$', 1105 '$<current_meds:: \item[%(substance)s] {\\footnotesize (%(brand)s)} %(preparation)s %(amount)s%(unit)s: %(schedule)s >$' 1106 ] 1107 1108 tests = [ 1109 1110 'junk $<lastname::::3>$ junk', 1111 'junk $<lastname::abc::3>$ junk', 1112 'junk $<lastname::abc>$ junk', 1113 'junk $<lastname>$ junk', 1114 1115 'junk $<lastname>$ junk $<firstname>$ junk', 1116 'junk $<lastname::abc>$ junk $<fiststname::abc>$ junk', 1117 'junk $<lastname::abc::3>$ junk $<firstname::abc::3>$ junk', 1118 'junk $<lastname::::3>$ junk $<firstname::::3>$ junk' 1119 1120 ] 1121 1122 print "testing placeholder regex:", default_placeholder_regex 1123 print "" 1124 1125 for t in tests: 1126 print 'line: "%s"' % t 1127 print "placeholders:" 1128 for p in regex.findall(default_placeholder_regex, t, regex.IGNORECASE): 1129 print ' => "%s"' % p 1130 print " "
1131 #--------------------------------------------------------
1132 - def test_placeholder():
1133 1134 #ph = u'emr_journal::soap //%(date)s %(modified_by)s %(soap_cat)s %(narrative)s//30::' 1135 ph = u'free_text::tex//placeholder test::9999' 1136 #ph = u'soap_for_encounters:://::9999' 1137 #ph = u'soap_a' 1138 #ph = u'encounter_list::%(started)s: %(assessment_of_encounter)s::30' 1139 1140 handler = gmPlaceholderHandler() 1141 handler.debug = True 1142 1143 pat = gmPersonSearch.ask_for_patient() 1144 if pat is None: 1145 return 1146 1147 gmPatSearchWidgets.set_active_patient(patient = pat) 1148 1149 app = wx.PyWidgetTester(size = (200, 50)) 1150 print u'%s => %s' % (ph, handler[ph])
1151 #-------------------------------------------------------- 1152 1153 #test_placeholders() 1154 #test_new_variant_placeholders() 1155 #test_scripting() 1156 #test_placeholder_regex() 1157 test_placeholder() 1158 1159 #===================================================================== 1160