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