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

Source Code for Module Gnumed.wxpython.gmMacro

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