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

Source Code for Module Gnumed.wxpython.gmMeasurementWidgets

   1  """GNUmed measurement widgets.""" 
   2  #================================================================ 
   3  __version__ = "$Revision: 1.66 $" 
   4  __author__ = "Karsten Hilbert <Karsten.Hilbert@gmx.net>" 
   5  __license__ = "GPL" 
   6   
   7   
   8  import sys, logging, datetime as pyDT, decimal, os, webbrowser, subprocess, codecs 
   9  import os.path 
  10   
  11   
  12  import wx, wx.grid, wx.lib.hyperlink 
  13   
  14   
  15  if __name__ == '__main__': 
  16          sys.path.insert(0, '../../') 
  17  from Gnumed.business import gmPerson 
  18  from Gnumed.business import gmPathLab 
  19  from Gnumed.business import gmSurgery 
  20  from Gnumed.business import gmLOINC 
  21  from Gnumed.business import gmForms 
  22  from Gnumed.business import gmPersonSearch 
  23   
  24  from Gnumed.pycommon import gmTools 
  25  from Gnumed.pycommon import gmNetworkTools 
  26  from Gnumed.pycommon import gmI18N 
  27  from Gnumed.pycommon import gmShellAPI 
  28  from Gnumed.pycommon import gmCfg 
  29  from Gnumed.pycommon import gmDateTime 
  30  from Gnumed.pycommon import gmMatchProvider 
  31  from Gnumed.pycommon import gmDispatcher 
  32   
  33  from Gnumed.wxpython import gmRegetMixin, gmPhraseWheel, gmEditArea, gmGuiHelpers, gmListWidgets 
  34  from Gnumed.wxpython import gmAuthWidgets, gmPatSearchWidgets, gmFormWidgets 
  35   
  36   
  37  _log = logging.getLogger('gm.ui') 
  38  _log.info(__version__) 
  39   
  40  #================================================================ 
  41  # LOINC related widgets 
  42  #================================================================ 
43 -def update_loinc_reference_data():
44 45 wx.BeginBusyCursor() 46 47 gmDispatcher.send(signal = 'statustext', msg = _('Updating LOINC data can take quite a while...'), beep = True) 48 49 # download 50 downloaded, loinc_dir = gmNetworkTools.download_data_pack(url = 'http://www.gnumed.de/downloads/data/loinc/loinctab.zip') 51 if not downloaded: 52 wx.EndBusyCursor() 53 gmGuiHelpers.gm_show_warning ( 54 aTitle = _('Downloading LOINC'), 55 aMessage = _('Error downloading the latest LOINC data.\n') 56 ) 57 return False 58 59 # split master data file 60 data_fname, license_fname = gmLOINC.split_LOINCDBTXT(input_fname = os.path.join(loinc_dir, 'LOINCDB.TXT')) 61 62 wx.EndBusyCursor() 63 64 conn = gmAuthWidgets.get_dbowner_connection(procedure = _('importing LOINC reference data')) 65 if conn is None: 66 return False 67 68 wx.BeginBusyCursor() 69 70 # import data 71 if gmLOINC.loinc_import(data_fname = data_fname, license_fname = license_fname, conn = conn): 72 gmDispatcher.send(signal = 'statustext', msg = _('Successfully imported LOINC reference data.')) 73 else: 74 gmDispatcher.send(signal = 'statustext', msg = _('Importing LOINC reference data failed.'), beep = True) 75 76 wx.EndBusyCursor() 77 return True
78 #================================================================ 79 # convenience functions 80 #================================================================
81 -def call_browser_on_measurement_type(measurement_type=None):
82 83 dbcfg = gmCfg.cCfgSQL() 84 85 url = dbcfg.get ( 86 option = u'external.urls.measurements_search', 87 workplace = gmSurgery.gmCurrentPractice().active_workplace, 88 bias = 'user', 89 default = u"http://www.google.de/search?as_oq=%(search_term)s&num=10&as_sitesearch=laborlexikon.de" 90 ) 91 92 base_url = dbcfg.get2 ( 93 option = u'external.urls.measurements_encyclopedia', 94 workplace = gmSurgery.gmCurrentPractice().active_workplace, 95 bias = 'user', 96 default = u'http://www.laborlexikon.de' 97 ) 98 99 if measurement_type is None: 100 url = base_url 101 102 measurement_type = measurement_type.strip() 103 104 if measurement_type == u'': 105 url = base_url 106 107 url = url % {'search_term': measurement_type} 108 109 webbrowser.open ( 110 url = url, 111 new = False, 112 autoraise = True 113 )
114 #----------------------------------------------------------------
115 -def edit_measurement(parent=None, measurement=None, single_entry=False):
116 ea = cMeasurementEditAreaPnl(parent = parent, id = -1) 117 ea.data = measurement 118 ea.mode = gmTools.coalesce(measurement, 'new', 'edit') 119 dlg = gmEditArea.cGenericEditAreaDlg2(parent = parent, id = -1, edit_area = ea, single_entry = single_entry) 120 dlg.SetTitle(gmTools.coalesce(measurement, _('Adding new measurement'), _('Editing measurement'))) 121 if dlg.ShowModal() == wx.ID_OK: 122 dlg.Destroy() 123 return True 124 dlg.Destroy() 125 return False
126 #================================================================
127 -def plot_measurements(parent=None, tests=None):
128 129 template = gmFormWidgets.manage_form_templates ( 130 parent = parent, 131 active_only = True, 132 template_types = [u'gnuplot script'] 133 ) 134 135 if template is None: 136 gmGuiHelpers.gm_show_error ( 137 aMessage = _('Cannot plot without a plot script.'), 138 aTitle = _('Plotting test results') 139 ) 140 return False 141 142 fname_data = gmPathLab.export_results_for_gnuplot(results = tests) 143 144 script = template.instantiate() 145 script.data_filename = fname_data 146 script.generate_output(format = 'wxp') # Gnuplot output terminal
147 148 #================================================================ 149 #from Gnumed.wxGladeWidgets import wxgPrimaryCareVitalsInputPnl 150 151 # Taillenumfang: Mitte zwischen unterster Rippe und 152 # hoechstem Teil des Beckenkamms 153 # Maenner: maessig: 94-102, deutlich: > 102 .. erhoeht 154 # Frauen: maessig: 80-88, deutlich: > 88 .. erhoeht 155 156 #================================================================ 157 # display widgets 158 #================================================================
159 -class cMeasurementsGrid(wx.grid.Grid):
160 """A grid class for displaying measurment results. 161 162 - does NOT listen to the currently active patient 163 - thereby it can display any patient at any time 164 """ 165 # FIXME: sort-by-battery 166 # FIXME: filter-by-battery 167 # FIXME: filter out empty 168 # FIXME: filter by tests of a selected date 169 # FIXME: dates DESC/ASC by cfg 170 # FIXME: mouse over column header: display date info
171 - def __init__(self, *args, **kwargs):
172 173 wx.grid.Grid.__init__(self, *args, **kwargs) 174 175 self.__patient = None 176 self.__cell_data = {} 177 self.__row_label_data = [] 178 179 self.__prev_row = None 180 self.__prev_col = None 181 self.__prev_label_row = None 182 self.__date_format = str((_('lab_grid_date_format::%Y\n%b %d')).lstrip('lab_grid_date_format::')) 183 184 self.__init_ui() 185 self.__register_events()
186 #------------------------------------------------------------ 187 # external API 188 #------------------------------------------------------------
189 - def delete_current_selection(self):
190 if not self.IsSelection(): 191 gmDispatcher.send(signal = u'statustext', msg = _('No results selected for deletion.')) 192 return True 193 194 selected_cells = self.get_selected_cells() 195 if len(selected_cells) > 20: 196 results = None 197 msg = _( 198 'There are %s results marked for deletion.\n' 199 '\n' 200 'Are you sure you want to delete these results ?' 201 ) % len(selected_cells) 202 else: 203 results = self.__cells_to_data(cells = selected_cells, exclude_multi_cells = False) 204 txt = u'\n'.join([ u'%s %s (%s): %s %s%s' % ( 205 r['clin_when'].strftime('%x %H:%M').decode(gmI18N.get_encoding()), 206 r['unified_abbrev'], 207 r['unified_name'], 208 r['unified_val'], 209 r['val_unit'], 210 gmTools.coalesce(r['abnormality_indicator'], u'', u' (%s)') 211 ) for r in results 212 ]) 213 msg = _( 214 'The following results are marked for deletion:\n' 215 '\n' 216 '%s\n' 217 '\n' 218 'Are you sure you want to delete these results ?' 219 ) % txt 220 221 dlg = gmGuiHelpers.c2ButtonQuestionDlg ( 222 self, 223 -1, 224 caption = _('Deleting test results'), 225 question = msg, 226 button_defs = [ 227 {'label': _('Delete'), 'tooltip': _('Yes, delete all the results.'), 'default': False}, 228 {'label': _('Cancel'), 'tooltip': _('No, do NOT delete any results.'), 'default': True} 229 ] 230 ) 231 decision = dlg.ShowModal() 232 233 if decision == wx.ID_YES: 234 if results is None: 235 results = self.__cells_to_data(cells = selected_cells, exclude_multi_cells = False) 236 for result in results: 237 gmPathLab.delete_test_result(result)
238 #------------------------------------------------------------
239 - def sign_current_selection(self):
240 if not self.IsSelection(): 241 gmDispatcher.send(signal = u'statustext', msg = _('Cannot sign results. No results selected.')) 242 return True 243 244 selected_cells = self.get_selected_cells() 245 if len(selected_cells) > 10: 246 test_count = len(selected_cells) 247 tests = None 248 else: 249 test_count = None 250 tests = self.__cells_to_data(cells = selected_cells, exclude_multi_cells = False) 251 if len(tests) == 0: 252 return True 253 254 dlg = cMeasurementsReviewDlg ( 255 self, 256 -1, 257 tests = tests, 258 test_count = test_count 259 ) 260 decision = dlg.ShowModal() 261 262 if decision == wx.ID_APPLY: 263 wx.BeginBusyCursor() 264 265 if dlg._RBTN_confirm_abnormal.GetValue(): 266 abnormal = None 267 elif dlg._RBTN_results_normal.GetValue(): 268 abnormal = False 269 else: 270 abnormal = True 271 272 if dlg._RBTN_confirm_relevance.GetValue(): 273 relevant = None 274 elif dlg._RBTN_results_not_relevant.GetValue(): 275 relevant = False 276 else: 277 relevant = True 278 279 if tests is None: 280 tests = self.__cells_to_data(cells = selected_cells, exclude_multi_cells = False) 281 282 comment = None 283 if len(tests) == 1: 284 comment = dlg._TCTRL_comment.GetValue() 285 286 for test in tests: 287 test.set_review ( 288 technically_abnormal = abnormal, 289 clinically_relevant = relevant, 290 comment = comment, 291 make_me_responsible = dlg._CHBOX_responsible.IsChecked() 292 ) 293 294 wx.EndBusyCursor() 295 296 dlg.Destroy()
297 #------------------------------------------------------------
298 - def plot_current_selection(self):
299 300 if not self.IsSelection(): 301 gmDispatcher.send(signal = u'statustext', msg = _('Cannot plot results. No results selected.')) 302 return True 303 304 tests = self.__cells_to_data ( 305 cells = self.get_selected_cells(), 306 exclude_multi_cells = False, 307 auto_include_multi_cells = True 308 ) 309 310 plot_measurements(parent = self, tests = tests)
311 #------------------------------------------------------------
312 - def get_selected_cells(self):
313 314 sel_block_top_left = self.GetSelectionBlockTopLeft() 315 sel_block_bottom_right = self.GetSelectionBlockBottomRight() 316 sel_cols = self.GetSelectedCols() 317 sel_rows = self.GetSelectedRows() 318 319 selected_cells = [] 320 321 # individually selected cells (ctrl-click) 322 selected_cells += self.GetSelectedCells() 323 324 # selected rows 325 selected_cells += list ( 326 (row, col) 327 for row in sel_rows 328 for col in xrange(self.GetNumberCols()) 329 ) 330 331 # selected columns 332 selected_cells += list ( 333 (row, col) 334 for row in xrange(self.GetNumberRows()) 335 for col in sel_cols 336 ) 337 338 # selection blocks 339 for top_left, bottom_right in zip(self.GetSelectionBlockTopLeft(), self.GetSelectionBlockBottomRight()): 340 selected_cells += [ 341 (row, col) 342 for row in xrange(top_left[0], bottom_right[0] + 1) 343 for col in xrange(top_left[1], bottom_right[1] + 1) 344 ] 345 346 return set(selected_cells)
347 #------------------------------------------------------------
348 - def select_cells(self, unsigned_only=False, accountables_only=False, keep_preselections=False):
349 """Select a range of cells according to criteria. 350 351 unsigned_only: include only those which are not signed at all yet 352 accountable_only: include only those for which the current user is responsible 353 keep_preselections: broaden (rather than replace) the range of selected cells 354 355 Combinations are powerful ! 356 """ 357 wx.BeginBusyCursor() 358 self.BeginBatch() 359 360 if not keep_preselections: 361 self.ClearSelection() 362 363 for col_idx in self.__cell_data.keys(): 364 for row_idx in self.__cell_data[col_idx].keys(): 365 # loop over results in cell and only include 366 # those multi-value cells that are not ambiguous 367 do_not_include = False 368 for result in self.__cell_data[col_idx][row_idx]: 369 if unsigned_only: 370 if result['reviewed']: 371 do_not_include = True 372 break 373 if accountables_only: 374 if not result['you_are_responsible']: 375 do_not_include = True 376 break 377 if do_not_include: 378 continue 379 380 self.SelectBlock(row_idx, col_idx, row_idx, col_idx, addToSelected = True) 381 382 self.EndBatch() 383 wx.EndBusyCursor()
384 #------------------------------------------------------------
385 - def repopulate_grid(self):
386 387 self.empty_grid() 388 if self.__patient is None: 389 return 390 391 emr = self.__patient.get_emr() 392 393 self.__row_label_data = emr.get_test_types_for_results() 394 test_type_labels = [ u'%s (%s)' % (test['unified_abbrev'], test['unified_name']) for test in self.__row_label_data ] 395 if len(test_type_labels) == 0: 396 return 397 398 test_date_labels = [ date[0].strftime(self.__date_format) for date in emr.get_dates_for_results() ] 399 results = emr.get_test_results_by_date() 400 401 self.BeginBatch() 402 403 # rows 404 self.AppendRows(numRows = len(test_type_labels)) 405 for row_idx in range(len(test_type_labels)): 406 self.SetRowLabelValue(row_idx, test_type_labels[row_idx]) 407 408 # columns 409 self.AppendCols(numCols = len(test_date_labels)) 410 for date_idx in range(len(test_date_labels)): 411 self.SetColLabelValue(date_idx, test_date_labels[date_idx]) 412 413 # cell values (list of test results) 414 for result in results: 415 row = test_type_labels.index(u'%s (%s)' % (result['unified_abbrev'], result['unified_name'])) 416 col = test_date_labels.index(result['clin_when'].strftime(self.__date_format)) 417 418 try: 419 self.__cell_data[col] 420 except KeyError: 421 self.__cell_data[col] = {} 422 423 # the tooltip always shows the youngest sub result details 424 if self.__cell_data[col].has_key(row): 425 self.__cell_data[col][row].append(result) 426 self.__cell_data[col][row].sort(key = lambda x: x['clin_when'], reverse = True) 427 else: 428 self.__cell_data[col][row] = [result] 429 430 # rebuild cell display string 431 vals2display = [] 432 for sub_result in self.__cell_data[col][row]: 433 434 # is the sub_result technically abnormal ? 435 ind = gmTools.coalesce(sub_result['abnormality_indicator'], u'').strip() 436 if ind != u'': 437 lab_abnormality_indicator = u' (%s)' % ind[:3] 438 else: 439 lab_abnormality_indicator = u'' 440 # - if noone reviewed - use what the lab thinks 441 if sub_result['is_technically_abnormal'] is None: 442 abnormality_indicator = lab_abnormality_indicator 443 # - if someone reviewed and decreed normality - use that 444 elif sub_result['is_technically_abnormal'] is False: 445 abnormality_indicator = u'' 446 # - if someone reviewed and decreed abnormality ... 447 else: 448 # ... invent indicator if the lab did't use one 449 if lab_abnormality_indicator == u'': 450 # FIXME: calculate from min/max/range 451 abnormality_indicator = u' (%s)' % gmTools.u_plus_minus 452 # ... else use indicator the lab used 453 else: 454 abnormality_indicator = lab_abnormality_indicator 455 456 # is the sub_result relevant clinically ? 457 # FIXME: take into account primary_GP once we support that 458 sub_result_relevant = sub_result['is_clinically_relevant'] 459 if sub_result_relevant is None: 460 # FIXME: calculate from clinical range 461 sub_result_relevant = False 462 463 missing_review = False 464 # warn on missing review if 465 # a) no review at all exists or 466 if not sub_result['reviewed']: 467 missing_review = True 468 # b) there is a review but 469 else: 470 # current user is reviewer and hasn't reviewed 471 if sub_result['you_are_responsible'] and not sub_result['review_by_you']: 472 missing_review = True 473 474 # can we display the full sub_result length ? 475 if len(sub_result['unified_val']) > 8: 476 tmp = u'%.7s%s' % (sub_result['unified_val'][:7], gmTools.u_ellipsis) 477 else: 478 tmp = u'%.8s' % sub_result['unified_val'][:8] 479 480 # abnormal ? 481 tmp = u'%s%.6s' % (tmp, abnormality_indicator) 482 483 # is there a comment ? 484 has_sub_result_comment = gmTools.coalesce ( 485 gmTools.coalesce(sub_result['note_test_org'], sub_result['comment']), 486 u'' 487 ).strip() != u'' 488 if has_sub_result_comment: 489 tmp = u'%s %s' % (tmp, gmTools.u_ellipsis) 490 491 # lacking a review ? 492 if missing_review: 493 tmp = u'%s %s' % (tmp, gmTools.u_writing_hand) 494 495 # part of a multi-result cell ? 496 if len(self.__cell_data[col][row]) > 1: 497 tmp = u'%s %s' % (sub_result['clin_when'].strftime('%H:%M'), tmp) 498 499 vals2display.append(tmp) 500 501 self.SetCellValue(row, col, u'\n'.join(vals2display)) 502 self.SetCellAlignment(row, col, horiz = wx.ALIGN_RIGHT, vert = wx.ALIGN_CENTRE) 503 # font = self.GetCellFont(row, col) 504 # if not font.IsFixedWidth(): 505 # font.SetFamily(family = wx.FONTFAMILY_MODERN) 506 # FIXME: what about partial sub results being relevant ?? 507 if sub_result_relevant: 508 font = self.GetCellFont(row, col) 509 self.SetCellTextColour(row, col, 'firebrick') 510 font.SetWeight(wx.FONTWEIGHT_BOLD) 511 self.SetCellFont(row, col, font) 512 # self.SetCellFont(row, col, font) 513 514 self.AutoSize() 515 self.EndBatch() 516 return
517 #------------------------------------------------------------
518 - def empty_grid(self):
519 self.BeginBatch() 520 self.ClearGrid() 521 # Windows cannot do nothing, it rather decides to assert() 522 # on thinking it is supposed to do nothing 523 if self.GetNumberRows() > 0: 524 self.DeleteRows(pos = 0, numRows = self.GetNumberRows()) 525 if self.GetNumberCols() > 0: 526 self.DeleteCols(pos = 0, numCols = self.GetNumberCols()) 527 self.EndBatch() 528 self.__cell_data = {} 529 self.__row_label_data = []
530 #------------------------------------------------------------
531 - def get_row_tooltip(self, row=None):
532 # display test info (unified, which tests are grouped, which panels they belong to 533 # include details about test types included, 534 # most recent value in this row, etc 535 # test_details, td_idx = emr.get_test_types_details() 536 537 # sometimes, for some reason, there is no row and 538 # wxPython still tries to find a tooltip for it 539 try: 540 tt = self.__row_label_data[row] 541 except IndexError: 542 return u' ' 543 544 tip = u'' 545 tip += _('Details about %s (%s)%s\n') % (tt['unified_name'], tt['unified_abbrev'], gmTools.coalesce(tt['unified_loinc'], u'', u' [%s]')) 546 tip += u'\n' 547 tip += _('Meta type:\n') 548 tip += _(' Name: %s (%s)%s #%s\n') % (tt['name_meta'], tt['abbrev_meta'], gmTools.coalesce(tt['loinc_meta'], u'', u' [%s]'), tt['pk_meta_test_type']) 549 tip += gmTools.coalesce(tt['conversion_unit'], u'', _(' Conversion unit: %s\n')) 550 tip += gmTools.coalesce(tt['comment_meta'], u'', _(' Comment: %s\n')) 551 tip += u'\n' 552 tip += _('Test type:\n') 553 tip += _(' Name: %s (%s)%s #%s\n') % (tt['name_tt'], tt['abbrev_tt'], gmTools.coalesce(tt['loinc_tt'], u'', u' [%s]'), tt['pk_test_type']) 554 tip += gmTools.coalesce(tt['comment_tt'], u'', _(' Comment: %s\n')) 555 tip += gmTools.coalesce(tt['code_tt'], u'', _(' Code: %s\n')) 556 tip += gmTools.coalesce(tt['coding_system_tt'], u'', _(' Code: %s\n')) 557 result = tt.get_most_recent_result(pk_patient = self.__patient.ID) 558 if result is not None: 559 tip += u'\n' 560 tip += _('Most recent result:\n') 561 tip += _(' %s: %s%s%s') % ( 562 result['clin_when'].strftime('%Y-%m-%d'), 563 result['unified_val'], 564 gmTools.coalesce(result['val_unit'], u'', u' %s'), 565 gmTools.coalesce(result['abnormality_indicator'], u'', u' (%s)') 566 ) 567 568 return tip
569 #------------------------------------------------------------
570 - def get_cell_tooltip(self, col=None, row=None):
571 # FIXME: add panel/battery, request details 572 573 try: 574 d = self.__cell_data[col][row] 575 except KeyError: 576 # FIXME: maybe display the most recent or when the most recent was ? 577 d = None 578 579 if d is None: 580 return u' ' 581 582 is_multi_cell = False 583 if len(d) > 1: 584 is_multi_cell = True 585 586 d = d[0] 587 588 has_normal_min_or_max = (d['val_normal_min'] is not None) or (d['val_normal_max'] is not None) 589 if has_normal_min_or_max: 590 normal_min_max = u'%s - %s' % ( 591 gmTools.coalesce(d['val_normal_min'], u'?'), 592 gmTools.coalesce(d['val_normal_max'], u'?') 593 ) 594 else: 595 normal_min_max = u'' 596 597 has_clinical_min_or_max = (d['val_target_min'] is not None) or (d['val_target_max'] is not None) 598 if has_clinical_min_or_max: 599 clinical_min_max = u'%s - %s' % ( 600 gmTools.coalesce(d['val_target_min'], u'?'), 601 gmTools.coalesce(d['val_target_max'], u'?') 602 ) 603 else: 604 clinical_min_max = u'' 605 606 # header 607 if is_multi_cell: 608 tt = _(u'Measurement details of most recent (topmost) result: \n') 609 else: 610 tt = _(u'Measurement details: \n') 611 612 # basics 613 tt += u' ' + _(u'Date: %s\n') % d['clin_when'].strftime('%c').decode(gmI18N.get_encoding()) 614 tt += u' ' + _(u'Type: "%(name)s" (%(code)s) [#%(pk_type)s]\n') % ({ 615 'name': d['name_tt'], 616 'code': d['code_tt'], 617 'pk_type': d['pk_test_type'] 618 }) 619 tt += u' ' + _(u'Result: %(val)s%(unit)s%(ind)s [#%(pk_result)s]\n') % ({ 620 'val': d['unified_val'], 621 'unit': gmTools.coalesce(d['val_unit'], u'', u' %s'), 622 'ind': gmTools.coalesce(d['abnormality_indicator'], u'', u' (%s)'), 623 'pk_result': d['pk_test_result'] 624 }) 625 tmp = (u'%s%s' % ( 626 gmTools.coalesce(d['name_test_org'], u''), 627 gmTools.coalesce(d['contact_test_org'], u'', u' (%s)'), 628 )).strip() 629 if tmp != u'': 630 tt += u' ' + _(u'Source: %s\n') % tmp 631 tt += u'\n' 632 633 # clinical evaluation 634 norm_eval = None 635 if d['val_num'] is not None: 636 # 1) normal range 637 # lowered ? 638 if (d['val_normal_min'] is not None) and (d['val_num'] < d['val_normal_min']): 639 try: 640 percent = (d['val_num'] * 100) / d['val_normal_min'] 641 except ZeroDivisionError: 642 percent = None 643 if percent is not None: 644 if percent < 6: 645 norm_eval = _(u'%.1f %% of the normal lower limit') % percent 646 else: 647 norm_eval = _(u'%.0f %% of the normal lower limit') % percent 648 # raised ? 649 if (d['val_normal_max'] is not None) and (d['val_num'] > d['val_normal_max']): 650 try: 651 x_times = d['val_num'] / d['val_normal_max'] 652 except ZeroDivisionError: 653 x_times = None 654 if x_times is not None: 655 if x_times < 10: 656 norm_eval = _(u'%.1f times the normal upper limit') % x_times 657 else: 658 norm_eval = _(u'%.0f times the normal upper limit') % x_times 659 if norm_eval is not None: 660 tt += u' (%s)\n' % norm_eval 661 # #------------------------------------- 662 # # this idea was shot down on the list 663 # #------------------------------------- 664 # # bandwidth of deviation 665 # if None not in [d['val_normal_min'], d['val_normal_max']]: 666 # normal_width = d['val_normal_max'] - d['val_normal_min'] 667 # deviation_from_normal_range = None 668 # # below ? 669 # if d['val_num'] < d['val_normal_min']: 670 # deviation_from_normal_range = d['val_normal_min'] - d['val_num'] 671 # # above ? 672 # elif d['val_num'] > d['val_normal_max']: 673 # deviation_from_normal_range = d['val_num'] - d['val_normal_max'] 674 # if deviation_from_normal_range is None: 675 # try: 676 # times_deviation = deviation_from_normal_range / normal_width 677 # except ZeroDivisionError: 678 # times_deviation = None 679 # if times_deviation is not None: 680 # if times_deviation < 10: 681 # tt += u' (%s)\n' % _(u'deviates by %.1f times of the normal range') % times_deviation 682 # else: 683 # tt += u' (%s)\n' % _(u'deviates by %.0f times of the normal range') % times_deviation 684 # #------------------------------------- 685 686 # 2) clinical target range 687 norm_eval = None 688 # lowered ? 689 if (d['val_target_min'] is not None) and (d['val_num'] < d['val_target_min']): 690 try: 691 percent = (d['val_num'] * 100) / d['val_target_min'] 692 except ZeroDivisionError: 693 percent = None 694 if percent is not None: 695 if percent < 6: 696 norm_eval = _(u'%.1f %% of the target lower limit') % percent 697 else: 698 norm_eval = _(u'%.0f %% of the target lower limit') % percent 699 # raised ? 700 if (d['val_target_max'] is not None) and (d['val_num'] > d['val_target_max']): 701 try: 702 x_times = d['val_num'] / d['val_target_max'] 703 except ZeroDivisionError: 704 x_times = None 705 if x_times is not None: 706 if x_times < 10: 707 norm_eval = _(u'%.1f times the target upper limit') % x_times 708 else: 709 norm_eval = _(u'%.0f times the target upper limit') % x_times 710 if norm_eval is not None: 711 tt += u' (%s)\n' % norm_eval 712 # #------------------------------------- 713 # # this idea was shot down on the list 714 # #------------------------------------- 715 # # bandwidth of deviation 716 # if None not in [d['val_target_min'], d['val_target_max']]: 717 # normal_width = d['val_target_max'] - d['val_target_min'] 718 # deviation_from_target_range = None 719 # # below ? 720 # if d['val_num'] < d['val_target_min']: 721 # deviation_from_target_range = d['val_target_min'] - d['val_num'] 722 # # above ? 723 # elif d['val_num'] > d['val_target_max']: 724 # deviation_from_target_range = d['val_num'] - d['val_target_max'] 725 # if deviation_from_target_range is None: 726 # try: 727 # times_deviation = deviation_from_target_range / normal_width 728 # except ZeroDivisionError: 729 # times_deviation = None 730 # if times_deviation is not None: 731 # if times_deviation < 10: 732 # tt += u' (%s)\n' % _(u'deviates by %.1f times of the target range') % times_deviation 733 # else: 734 # tt += u' (%s)\n' % _(u'deviates by %.0f times of the target range') % times_deviation 735 # #------------------------------------- 736 737 # ranges 738 tt += u' ' + _(u'Standard normal range: %(norm_min_max)s%(norm_range)s \n') % ({ 739 'norm_min_max': normal_min_max, 740 'norm_range': gmTools.coalesce ( 741 d['val_normal_range'], 742 u'', 743 gmTools.bool2subst ( 744 has_normal_min_or_max, 745 u' / %s', 746 u'%s' 747 ) 748 ) 749 }) 750 if d['norm_ref_group'] is not None: 751 tt += u' ' + _(u'Reference group: %s\n') % d['norm_ref_group'] 752 tt += u' ' + _(u'Clinical target range: %(clin_min_max)s%(clin_range)s \n') % ({ 753 'clin_min_max': clinical_min_max, 754 'clin_range': gmTools.coalesce ( 755 d['val_target_range'], 756 u'', 757 gmTools.bool2subst ( 758 has_clinical_min_or_max, 759 u' / %s', 760 u'%s' 761 ) 762 ) 763 }) 764 765 # metadata 766 if d['comment'] is not None: 767 tt += u' ' + _(u'Doc: %s\n') % _(u'\n Doc: ').join(d['comment'].split(u'\n')) 768 if d['note_test_org'] is not None: 769 tt += u' ' + _(u'Lab: %s\n') % _(u'\n Lab: ').join(d['note_test_org'].split(u'\n')) 770 tt += u' ' + _(u'Episode: %s\n') % d['episode'] 771 if d['health_issue'] is not None: 772 tt += u' ' + _(u'Issue: %s\n') % d['health_issue'] 773 if d['material'] is not None: 774 tt += u' ' + _(u'Material: %s\n') % d['material'] 775 if d['material_detail'] is not None: 776 tt += u' ' + _(u'Details: %s\n') % d['material_detail'] 777 tt += u'\n' 778 779 # review 780 if d['reviewed']: 781 review = d['last_reviewed'].strftime('%c').decode(gmI18N.get_encoding()) 782 else: 783 review = _('not yet') 784 tt += _(u'Signed (%(sig_hand)s): %(reviewed)s\n') % ({ 785 'sig_hand': gmTools.u_writing_hand, 786 'reviewed': review 787 }) 788 tt += u' ' + _(u'Responsible clinician: %s\n') % gmTools.bool2subst(d['you_are_responsible'], _('you'), d['responsible_reviewer']) 789 if d['reviewed']: 790 tt += u' ' + _(u'Last reviewer: %(reviewer)s\n') % ({'reviewer': gmTools.bool2subst(d['review_by_you'], _('you'), gmTools.coalesce(d['last_reviewer'], u'?'))}) 791 tt += u' ' + _(u' Technically abnormal: %(abnormal)s\n') % ({'abnormal': gmTools.bool2subst(d['is_technically_abnormal'], _('yes'), _('no'), u'?')}) 792 tt += u' ' + _(u' Clinically relevant: %(relevant)s\n') % ({'relevant': gmTools.bool2subst(d['is_clinically_relevant'], _('yes'), _('no'), u'?')}) 793 if d['review_comment'] is not None: 794 tt += u' ' + _(u' Comment: %s\n') % d['review_comment'].strip() 795 tt += u'\n' 796 797 # type 798 tt += _(u'Test type details:\n') 799 tt += u' ' + _(u'Grouped under "%(name_meta)s" (%(abbrev_meta)s) [#%(pk_u_type)s]\n') % ({ 800 'name_meta': gmTools.coalesce(d['name_meta'], u''), 801 'abbrev_meta': gmTools.coalesce(d['abbrev_meta'], u''), 802 'pk_u_type': d['pk_meta_test_type'] 803 }) 804 if d['comment_tt'] is not None: 805 tt += u' ' + _(u'Type comment: %s\n') % _(u'\n Type comment:').join(d['comment_tt'].split(u'\n')) 806 if d['comment_meta'] is not None: 807 tt += u' ' + _(u'Group comment: %s\n') % _(u'\n Group comment: ').join(d['comment_meta'].split(u'\n')) 808 tt += u'\n' 809 810 tt += _(u'Revisions: %(row_ver)s, last %(mod_when)s by %(mod_by)s.') % ({ 811 'row_ver': d['row_version'], 812 'mod_when': d['modified_when'].strftime('%c').decode(gmI18N.get_encoding()), 813 'mod_by': d['modified_by'] 814 }) 815 816 return tt
817 #------------------------------------------------------------ 818 # internal helpers 819 #------------------------------------------------------------
820 - def __init_ui(self):
821 self.CreateGrid(0, 1) 822 self.EnableEditing(0) 823 self.EnableDragGridSize(1) 824 825 # setting this screws up the labels: they are cut off and displaced 826 #self.SetColLabelAlignment(wx.ALIGN_CENTER, wx.ALIGN_BOTTOM) 827 828 #self.SetRowLabelSize(wx.GRID_AUTOSIZE) # starting with 2.8.8 829 self.SetRowLabelSize(150) 830 self.SetRowLabelAlignment(horiz = wx.ALIGN_LEFT, vert = wx.ALIGN_CENTRE) 831 832 # add link to left upper corner 833 dbcfg = gmCfg.cCfgSQL() 834 url = dbcfg.get2 ( 835 option = u'external.urls.measurements_encyclopedia', 836 workplace = gmSurgery.gmCurrentPractice().active_workplace, 837 bias = 'user', 838 default = u'http://www.laborlexikon.de' 839 ) 840 841 self.__WIN_corner = self.GetGridCornerLabelWindow() # a wx.Window instance 842 843 LNK_lab = wx.lib.hyperlink.HyperLinkCtrl ( 844 self.__WIN_corner, 845 -1, 846 label = _('Reference'), 847 style = wx.HL_DEFAULT_STYLE # wx.TE_READONLY|wx.TE_CENTRE| wx.NO_BORDER | 848 ) 849 LNK_lab.SetURL(url) 850 LNK_lab.SetBackgroundColour(wx.SystemSettings_GetColour(wx.SYS_COLOUR_BACKGROUND)) 851 LNK_lab.SetToolTipString(_( 852 'Navigate to an encyclopedia of measurements\n' 853 'and test methods on the web.\n' 854 '\n' 855 ' <%s>' 856 ) % url) 857 858 SZR_inner = wx.BoxSizer(wx.HORIZONTAL) 859 SZR_inner.Add((20, 20), 1, wx.EXPAND, 0) # spacer 860 SZR_inner.Add(LNK_lab, 0, wx.ALIGN_CENTER_VERTICAL, 0) #wx.ALIGN_CENTER wx.EXPAND 861 SZR_inner.Add((20, 20), 1, wx.EXPAND, 0) # spacer 862 863 SZR_corner = wx.BoxSizer(wx.VERTICAL) 864 SZR_corner.Add((20, 20), 1, wx.EXPAND, 0) # spacer 865 SZR_corner.AddWindow(SZR_inner, 0, wx.EXPAND) # inner sizer with centered hyperlink 866 SZR_corner.Add((20, 20), 1, wx.EXPAND, 0) # spacer 867 868 self.__WIN_corner.SetSizer(SZR_corner) 869 SZR_corner.Fit(self.__WIN_corner)
870 #------------------------------------------------------------
871 - def __resize_corner_window(self, evt):
872 self.__WIN_corner.Layout()
873 #------------------------------------------------------------
874 - def __cells_to_data(self, cells=None, exclude_multi_cells=False, auto_include_multi_cells=False):
875 """List of <cells> must be in row / col order.""" 876 data = [] 877 for row, col in cells: 878 try: 879 # cell data is stored col / row 880 data_list = self.__cell_data[col][row] 881 except KeyError: 882 continue 883 884 if len(data_list) == 1: 885 data.append(data_list[0]) 886 continue 887 888 if exclude_multi_cells: 889 gmDispatcher.send(signal = u'statustext', msg = _('Excluding multi-result field from further processing.')) 890 continue 891 892 if auto_include_multi_cells: 893 data.extend(data_list) 894 continue 895 896 data_to_include = self.__get_choices_from_multi_cell(cell_data = data_list) 897 if data_to_include is None: 898 continue 899 data.extend(data_to_include) 900 901 return data
902 #------------------------------------------------------------
903 - def __get_choices_from_multi_cell(self, cell_data=None, single_selection=False):
904 data = gmListWidgets.get_choices_from_list ( 905 parent = self, 906 msg = _( 907 'Your selection includes a field with multiple results.\n' 908 '\n' 909 'Please select the individual results you want to work on:' 910 ), 911 caption = _('Selecting test results'), 912 choices = [ [d['clin_when'], d['unified_abbrev'], d['unified_name'], d['unified_val']] for d in cell_data ], 913 columns = [_('Date / Time'), _('Code'), _('Test'), _('Result')], 914 data = cell_data, 915 single_selection = single_selection 916 ) 917 return data
918 #------------------------------------------------------------ 919 # event handling 920 #------------------------------------------------------------
921 - def __register_events(self):
922 # dynamic tooltips: GridWindow, GridRowLabelWindow, GridColLabelWindow, GridCornerLabelWindow 923 self.GetGridWindow().Bind(wx.EVT_MOTION, self.__on_mouse_over_cells) 924 self.GetGridRowLabelWindow().Bind(wx.EVT_MOTION, self.__on_mouse_over_row_labels) 925 #self.GetGridColLabelWindow().Bind(wx.EVT_MOTION, self.__on_mouse_over_col_labels) 926 927 # sizing left upper corner window 928 self.Bind(wx.EVT_SIZE, self.__resize_corner_window) 929 930 # editing cells 931 self.Bind(wx.grid.EVT_GRID_CELL_LEFT_DCLICK, self.__on_cell_left_dclicked)
932 #------------------------------------------------------------
933 - def __on_cell_left_dclicked(self, evt):
934 col = evt.GetCol() 935 row = evt.GetRow() 936 937 # empty cell, perhaps ? 938 try: 939 self.__cell_data[col][row] 940 except KeyError: 941 # FIXME: invoke editor for adding value for day of that column 942 # FIMXE: and test of that row 943 return 944 945 if len(self.__cell_data[col][row]) > 1: 946 data = self.__get_choices_from_multi_cell(cell_data = self.__cell_data[col][row], single_selection = True) 947 else: 948 data = self.__cell_data[col][row][0] 949 950 if data is None: 951 return 952 953 edit_measurement(parent = self, measurement = data, single_entry = True)
954 #------------------------------------------------------------ 955 # def OnMouseMotionRowLabel(self, evt): 956 # x, y = self.CalcUnscrolledPosition(evt.GetPosition()) 957 # row = self.YToRow(y) 958 # label = self.table().GetRowHelpValue(row) 959 # self.GetGridRowLabelWindow().SetToolTipString(label or "") 960 # evt.Skip()
961 - def __on_mouse_over_row_labels(self, evt):
962 963 # Use CalcUnscrolledPosition() to get the mouse position within the 964 # entire grid including what's offscreen 965 x, y = self.CalcUnscrolledPosition(evt.GetX(), evt.GetY()) 966 967 row = self.YToRow(y) 968 969 if self.__prev_label_row == row: 970 return 971 972 self.__prev_label_row == row 973 974 evt.GetEventObject().SetToolTipString(self.get_row_tooltip(row = row))
975 #------------------------------------------------------------ 976 # def OnMouseMotionColLabel(self, evt): 977 # x, y = self.CalcUnscrolledPosition(evt.GetPosition()) 978 # col = self.XToCol(x) 979 # label = self.table().GetColHelpValue(col) 980 # self.GetGridColLabelWindow().SetToolTipString(label or "") 981 # evt.Skip() 982 #------------------------------------------------------------
983 - def __on_mouse_over_cells(self, evt):
984 """Calculate where the mouse is and set the tooltip dynamically.""" 985 986 # Use CalcUnscrolledPosition() to get the mouse position within the 987 # entire grid including what's offscreen 988 x, y = self.CalcUnscrolledPosition(evt.GetX(), evt.GetY()) 989 990 # use this logic to prevent tooltips outside the actual cells 991 # apply to GetRowSize, too 992 # tot = 0 993 # for col in xrange(self.NumberCols): 994 # tot += self.GetColSize(col) 995 # if xpos <= tot: 996 # self.tool_tip.Tip = 'Tool tip for Column %s' % ( 997 # self.GetColLabelValue(col)) 998 # break 999 # else: # mouse is in label area beyond the right-most column 1000 # self.tool_tip.Tip = '' 1001 1002 row, col = self.XYToCell(x, y) 1003 1004 if (row == self.__prev_row) and (col == self.__prev_col): 1005 return 1006 1007 self.__prev_row = row 1008 self.__prev_col = col 1009 1010 evt.GetEventObject().SetToolTipString(self.get_cell_tooltip(col=col, row=row))
1011 #------------------------------------------------------------ 1012 # properties 1013 #------------------------------------------------------------
1014 - def _set_patient(self, patient):
1015 self.__patient = patient 1016 self.repopulate_grid()
1017 1018 patient = property(lambda x:x, _set_patient)
1019 #================================================================ 1020 from Gnumed.wxGladeWidgets import wxgMeasurementsPnl 1021
1022 -class cMeasurementsPnl(wxgMeasurementsPnl.wxgMeasurementsPnl, gmRegetMixin.cRegetOnPaintMixin):
1023 """Panel holding a grid with lab data. Used as notebook page.""" 1024
1025 - def __init__(self, *args, **kwargs):
1026 1027 wxgMeasurementsPnl.wxgMeasurementsPnl.__init__(self, *args, **kwargs) 1028 gmRegetMixin.cRegetOnPaintMixin.__init__(self) 1029 self.__init_ui() 1030 self.__register_interests()
1031 #-------------------------------------------------------- 1032 # event handling 1033 #--------------------------------------------------------
1034 - def __register_interests(self):
1035 gmDispatcher.connect(signal = u'pre_patient_selection', receiver = self._on_pre_patient_selection) 1036 gmDispatcher.connect(signal = u'post_patient_selection', receiver = self._on_post_patient_selection) 1037 gmDispatcher.connect(signal = u'test_result_mod_db', receiver = self._schedule_data_reget) 1038 gmDispatcher.connect(signal = u'reviewed_test_results_mod_db', receiver = self._schedule_data_reget)
1039 #--------------------------------------------------------
1040 - def _on_post_patient_selection(self):
1041 wx.CallAfter(self.__on_post_patient_selection)
1042 #--------------------------------------------------------
1043 - def __on_post_patient_selection(self):
1044 self._schedule_data_reget()
1045 #--------------------------------------------------------
1046 - def _on_pre_patient_selection(self):
1047 wx.CallAfter(self.__on_pre_patient_selection)
1048 #--------------------------------------------------------
1049 - def __on_pre_patient_selection(self):
1050 self.data_grid.patient = None
1051 #--------------------------------------------------------
1052 - def _on_add_button_pressed(self, event):
1053 edit_measurement(parent = self, measurement = None)
1054 #--------------------------------------------------------
1055 - def _on_review_button_pressed(self, evt):
1056 self.PopupMenu(self.__action_button_popup)
1057 #--------------------------------------------------------
1058 - def _on_select_button_pressed(self, evt):
1059 if self._RBTN_my_unsigned.GetValue() is True: 1060 self.data_grid.select_cells(unsigned_only = True, accountables_only = True, keep_preselections = False) 1061 elif self._RBTN_all_unsigned.GetValue() is True: 1062 self.data_grid.select_cells(unsigned_only = True, accountables_only = False, keep_preselections = False)
1063 #--------------------------------------------------------
1064 - def __on_sign_current_selection(self, evt):
1065 self.data_grid.sign_current_selection()
1066 #--------------------------------------------------------
1067 - def __on_plot_current_selection(self, evt):
1068 self.data_grid.plot_current_selection()
1069 #--------------------------------------------------------
1070 - def __on_delete_current_selection(self, evt):
1071 self.data_grid.delete_current_selection()
1072 #-------------------------------------------------------- 1073 # internal API 1074 #--------------------------------------------------------
1075 - def __init_ui(self):
1076 self.__action_button_popup = wx.Menu(title = _('Perform on selected results:')) 1077 1078 menu_id = wx.NewId() 1079 self.__action_button_popup.AppendItem(wx.MenuItem(self.__action_button_popup, menu_id, _('Review and &sign'))) 1080 wx.EVT_MENU(self.__action_button_popup, menu_id, self.__on_sign_current_selection) 1081 1082 menu_id = wx.NewId() 1083 self.__action_button_popup.AppendItem(wx.MenuItem(self.__action_button_popup, menu_id, _('Plot'))) 1084 wx.EVT_MENU(self.__action_button_popup, menu_id, self.__on_plot_current_selection) 1085 1086 menu_id = wx.NewId() 1087 self.__action_button_popup.AppendItem(wx.MenuItem(self.__action_button_popup, menu_id, _('Export to &file'))) 1088 #wx.EVT_MENU(self.__action_button_popup, menu_id, self.data_grid.current_selection_to_file) 1089 self.__action_button_popup.Enable(id = menu_id, enable = False) 1090 1091 menu_id = wx.NewId() 1092 self.__action_button_popup.AppendItem(wx.MenuItem(self.__action_button_popup, menu_id, _('Export to &clipboard'))) 1093 #wx.EVT_MENU(self.__action_button_popup, menu_id, self.data_grid.current_selection_to_clipboard) 1094 self.__action_button_popup.Enable(id = menu_id, enable = False) 1095 1096 menu_id = wx.NewId() 1097 self.__action_button_popup.AppendItem(wx.MenuItem(self.__action_button_popup, menu_id, _('&Delete'))) 1098 wx.EVT_MENU(self.__action_button_popup, menu_id, self.__on_delete_current_selection)
1099 1100 # FIXME: create inbox message to staff to phone patient to come in 1101 # FIXME: generate and let edit a SOAP narrative and include the values 1102 1103 #-------------------------------------------------------- 1104 # reget mixin API 1105 #--------------------------------------------------------
1106 - def _populate_with_data(self):
1107 """Populate fields in pages with data from model.""" 1108 pat = gmPerson.gmCurrentPatient() 1109 if pat.connected: 1110 self.data_grid.patient = pat 1111 else: 1112 self.data_grid.patient = None 1113 return True
1114 #================================================================ 1115 # editing widgets 1116 #================================================================ 1117 from Gnumed.wxGladeWidgets import wxgMeasurementsReviewDlg 1118
1119 -class cMeasurementsReviewDlg(wxgMeasurementsReviewDlg.wxgMeasurementsReviewDlg):
1120
1121 - def __init__(self, *args, **kwargs):
1122 1123 try: 1124 tests = kwargs['tests'] 1125 del kwargs['tests'] 1126 test_count = len(tests) 1127 try: del kwargs['test_count'] 1128 except KeyError: pass 1129 except KeyError: 1130 tests = None 1131 test_count = kwargs['test_count'] 1132 del kwargs['test_count'] 1133 1134 wxgMeasurementsReviewDlg.wxgMeasurementsReviewDlg.__init__(self, *args, **kwargs) 1135 1136 if tests is None: 1137 msg = _('%s results selected. Too many to list individually.') % test_count 1138 else: 1139 msg = ' // '.join ( 1140 [ u'%s: %s %s (%s)' % ( 1141 t['unified_abbrev'], 1142 t['unified_val'], 1143 t['val_unit'], 1144 t['clin_when'].strftime('%x').decode(gmI18N.get_encoding()) 1145 ) for t in tests 1146 ] 1147 ) 1148 1149 self._LBL_tests.SetLabel(msg) 1150 1151 if test_count == 1: 1152 self._TCTRL_comment.Enable(True) 1153 self._TCTRL_comment.SetValue(gmTools.coalesce(tests[0]['review_comment'], u'')) 1154 if tests[0]['you_are_responsible']: 1155 self._CHBOX_responsible.Enable(False) 1156 1157 self.Fit()
1158 #-------------------------------------------------------- 1159 # event handling 1160 #--------------------------------------------------------
1161 - def _on_signoff_button_pressed(self, evt):
1162 if self.IsModal(): 1163 self.EndModal(wx.ID_APPLY) 1164 else: 1165 self.Close()
1166 #================================================================ 1167 from Gnumed.wxGladeWidgets import wxgMeasurementEditAreaPnl 1168
1169 -class cMeasurementEditAreaPnl(wxgMeasurementEditAreaPnl.wxgMeasurementEditAreaPnl, gmEditArea.cGenericEditAreaMixin):
1170 """This edit area saves *new* measurements into the active patient only.""" 1171
1172 - def __init__(self, *args, **kwargs):
1173 1174 try: 1175 self.__default_date = kwargs['date'] 1176 del kwargs['date'] 1177 except KeyError: 1178 self.__default_date = None 1179 1180 wxgMeasurementEditAreaPnl.wxgMeasurementEditAreaPnl.__init__(self, *args, **kwargs) 1181 gmEditArea.cGenericEditAreaMixin.__init__(self) 1182 1183 self.__register_interests() 1184 1185 self.successful_save_msg = _('Successfully saved measurement.') 1186 1187 self._DPRW_evaluated.display_accuracy = gmDateTime.acc_minutes
1188 #-------------------------------------------------------- 1189 # generic edit area mixin API 1190 #--------------------------------------------------------
1191 - def _refresh_as_new(self):
1192 self._PRW_test.SetText(u'', None, True) 1193 self.__refresh_loinc_info() 1194 self.__update_units_context() 1195 self._TCTRL_result.SetValue(u'') 1196 self._PRW_units.SetText(u'', None, True) 1197 self._PRW_abnormality_indicator.SetText(u'', None, True) 1198 if self.__default_date is None: 1199 self._DPRW_evaluated.SetData(data = pyDT.datetime.now(tz = gmDateTime.gmCurrentLocalTimezone)) 1200 else: 1201 self._DPRW_evaluated.SetData(data = None) 1202 self._TCTRL_note_test_org.SetValue(u'') 1203 self._PRW_intended_reviewer.SetData(gmPerson.gmCurrentProvider()['pk_staff']) 1204 self._PRW_problem.SetData() 1205 self._TCTRL_narrative.SetValue(u'') 1206 self._CHBOX_review.SetValue(False) 1207 self._CHBOX_abnormal.SetValue(False) 1208 self._CHBOX_relevant.SetValue(False) 1209 self._CHBOX_abnormal.Enable(False) 1210 self._CHBOX_relevant.Enable(False) 1211 self._TCTRL_review_comment.SetValue(u'') 1212 self._TCTRL_normal_min.SetValue(u'') 1213 self._TCTRL_normal_max.SetValue(u'') 1214 self._TCTRL_normal_range.SetValue(u'') 1215 self._TCTRL_target_min.SetValue(u'') 1216 self._TCTRL_target_max.SetValue(u'') 1217 self._TCTRL_target_range.SetValue(u'') 1218 self._TCTRL_norm_ref_group.SetValue(u'') 1219 1220 self._PRW_test.SetFocus()
1221 #--------------------------------------------------------
1222 - def _refresh_from_existing(self):
1223 self._PRW_test.SetData(data = self.data['pk_test_type']) 1224 self.__refresh_loinc_info() 1225 self.__update_units_context() 1226 self._TCTRL_result.SetValue(self.data['unified_val']) 1227 self._PRW_units.SetText(self.data['val_unit'], self.data['val_unit'], True) 1228 self._PRW_abnormality_indicator.SetText ( 1229 gmTools.coalesce(self.data['abnormality_indicator'], u''), 1230 gmTools.coalesce(self.data['abnormality_indicator'], u''), 1231 True 1232 ) 1233 self._DPRW_evaluated.SetData(data = self.data['clin_when']) 1234 self._TCTRL_note_test_org.SetValue(gmTools.coalesce(self.data['note_test_org'], u'')) 1235 self._PRW_intended_reviewer.SetData(self.data['pk_intended_reviewer']) 1236 self._PRW_problem.SetData(self.data['pk_episode']) 1237 self._TCTRL_narrative.SetValue(gmTools.coalesce(self.data['comment'], u'')) 1238 self._CHBOX_review.SetValue(False) 1239 self._CHBOX_abnormal.SetValue(gmTools.coalesce(self.data['is_technically_abnormal'], False)) 1240 self._CHBOX_relevant.SetValue(gmTools.coalesce(self.data['is_clinically_relevant'], False)) 1241 self._CHBOX_abnormal.Enable(False) 1242 self._CHBOX_relevant.Enable(False) 1243 self._TCTRL_review_comment.SetValue(gmTools.coalesce(self.data['review_comment'], u'')) 1244 self._TCTRL_normal_min.SetValue(unicode(gmTools.coalesce(self.data['val_normal_min'], u''))) 1245 self._TCTRL_normal_max.SetValue(unicode(gmTools.coalesce(self.data['val_normal_max'], u''))) 1246 self._TCTRL_normal_range.SetValue(gmTools.coalesce(self.data['val_normal_range'], u'')) 1247 self._TCTRL_target_min.SetValue(unicode(gmTools.coalesce(self.data['val_target_min'], u''))) 1248 self._TCTRL_target_max.SetValue(unicode(gmTools.coalesce(self.data['val_target_max'], u''))) 1249 self._TCTRL_target_range.SetValue(gmTools.coalesce(self.data['val_target_range'], u'')) 1250 self._TCTRL_norm_ref_group.SetValue(gmTools.coalesce(self.data['norm_ref_group'], u'')) 1251 1252 self._TCTRL_result.SetFocus()
1253 #--------------------------------------------------------
1255 self._refresh_from_existing() 1256 1257 self._PRW_test.SetText(u'', None, True) 1258 self.__refresh_loinc_info() 1259 self.__update_units_context() 1260 self._TCTRL_result.SetValue(u'') 1261 self._PRW_units.SetText(u'', None, True) 1262 self._PRW_abnormality_indicator.SetText(u'', None, True) 1263 # self._DPRW_evaluated 1264 self._TCTRL_note_test_org.SetValue(u'') 1265 self._TCTRL_narrative.SetValue(u'') 1266 self._CHBOX_review.SetValue(False) 1267 self._CHBOX_abnormal.SetValue(False) 1268 self._CHBOX_relevant.SetValue(False) 1269 self._CHBOX_abnormal.Enable(False) 1270 self._CHBOX_relevant.Enable(False) 1271 self._TCTRL_review_comment.SetValue(u'') 1272 self._TCTRL_normal_min.SetValue(u'') 1273 self._TCTRL_normal_max.SetValue(u'') 1274 self._TCTRL_normal_range.SetValue(u'') 1275 self._TCTRL_target_min.SetValue(u'') 1276 self._TCTRL_target_max.SetValue(u'') 1277 self._TCTRL_target_range.SetValue(u'') 1278 self._TCTRL_norm_ref_group.SetValue(u'') 1279 1280 self._PRW_test.SetFocus()
1281 #--------------------------------------------------------
1282 - def _valid_for_save(self):
1283 1284 validity = True 1285 1286 if not self._DPRW_evaluated.is_valid_timestamp(): 1287 self._DPRW_evaluated.display_as_valid(False) 1288 validity = False 1289 else: 1290 self._DPRW_evaluated.display_as_valid(True) 1291 1292 if self._TCTRL_result.GetValue().strip() == u'': 1293 validity = False 1294 self.display_ctrl_as_valid(self._TCTRL_result, False) 1295 else: 1296 self.display_ctrl_as_valid(self._TCTRL_result, True) 1297 1298 if self._PRW_problem.GetValue().strip() == u'': 1299 self._PRW_problem.display_as_valid(False) 1300 validity = False 1301 else: 1302 self._PRW_problem.display_as_valid(True) 1303 1304 if self._PRW_test.GetValue().strip() == u'': 1305 self._PRW_test.display_as_valid(False) 1306 validity = False 1307 else: 1308 self._PRW_test.display_as_valid(True) 1309 1310 if self._PRW_intended_reviewer.GetData() is None: 1311 self._PRW_intended_reviewer.display_as_valid(False) 1312 validity = False 1313 else: 1314 self._PRW_intended_reviewer.display_as_valid(True) 1315 1316 if self._PRW_units.GetValue().strip() == u'': 1317 self._PRW_units.display_as_valid(False) 1318 validity = False 1319 else: 1320 self._PRW_units.display_as_valid(True) 1321 1322 ctrls = [self._TCTRL_normal_min, self._TCTRL_normal_max, self._TCTRL_target_min, self._TCTRL_target_max] 1323 for widget in ctrls: 1324 val = widget.GetValue().strip() 1325 if val == u'': 1326 continue 1327 try: 1328 decimal.Decimal(val.replace(',', u'.', 1)) 1329 self.display_ctrl_as_valid(widget, True) 1330 except: 1331 validity = False 1332 self.display_ctrl_as_valid(widget, False) 1333 1334 if validity is False: 1335 gmDispatcher.send(signal = 'statustext', msg = _('Cannot save result. Invalid or missing essential input.')) 1336 1337 return validity
1338 #--------------------------------------------------------
1339 - def _save_as_new(self):
1340 1341 emr = gmPerson.gmCurrentPatient().get_emr() 1342 1343 success, result = gmTools.input2decimal(self._TCTRL_result.GetValue()) 1344 if success: 1345 v_num = result 1346 v_al = None 1347 else: 1348 v_al = self._TCTRL_result.GetValue().strip() 1349 v_num = None 1350 1351 pk_type = self._PRW_test.GetData() 1352 if pk_type is None: 1353 tt = gmPathLab.create_measurement_type ( 1354 lab = None, 1355 abbrev = self._PRW_test.GetValue().strip(), 1356 name = self._PRW_test.GetValue().strip(), 1357 unit = gmTools.coalesce(self._PRW_units.GetData(), self._PRW_units.GetValue()).strip() 1358 ) 1359 pk_type = tt['pk_test_type'] 1360 1361 tr = emr.add_test_result ( 1362 episode = self._PRW_problem.GetData(can_create=True, is_open=False), 1363 type = pk_type, 1364 intended_reviewer = self._PRW_intended_reviewer.GetData(), 1365 val_num = v_num, 1366 val_alpha = v_al, 1367 unit = self._PRW_units.GetValue() 1368 ) 1369 1370 tr['clin_when'] = self._DPRW_evaluated.GetData().get_pydt() 1371 1372 ctrls = [ 1373 ('abnormality_indicator', self._PRW_abnormality_indicator), 1374 ('note_test_org', self._TCTRL_note_test_org), 1375 ('comment', self._TCTRL_narrative), 1376 ('val_normal_range', self._TCTRL_normal_range), 1377 ('val_target_range', self._TCTRL_target_range), 1378 ('norm_ref_group', self._TCTRL_norm_ref_group) 1379 ] 1380 for field, widget in ctrls: 1381 tr[field] = widget.GetValue().strip() 1382 1383 ctrls = [ 1384 ('val_normal_min', self._TCTRL_normal_min), 1385 ('val_normal_max', self._TCTRL_normal_max), 1386 ('val_target_min', self._TCTRL_target_min), 1387 ('val_target_max', self._TCTRL_target_max) 1388 ] 1389 for field, widget in ctrls: 1390 val = widget.GetValue().strip() 1391 if val == u'': 1392 tr[field] = None 1393 else: 1394 tr[field] = decimal.Decimal(val.replace(',', u'.', 1)) 1395 1396 tr.save_payload() 1397 1398 if self._CHBOX_review.GetValue() is True: 1399 tr.set_review ( 1400 technically_abnormal = self._CHBOX_abnormal.GetValue(), 1401 clinically_relevant = self._CHBOX_relevant.GetValue(), 1402 comment = gmTools.none_if(self._TCTRL_review_comment.GetValue().strip(), u''), 1403 make_me_responsible = False 1404 ) 1405 1406 self.data = tr 1407 1408 return True
1409 #--------------------------------------------------------
1410 - def _save_as_update(self):
1411 1412 success, result = gmTools.input2decimal(self._TCTRL_result.GetValue()) 1413 if success: 1414 v_num = result 1415 v_al = None 1416 else: 1417 v_num = None 1418 v_al = self._TCTRL_result.GetValue().strip() 1419 1420 pk_type = self._PRW_test.GetData() 1421 if pk_type is None: 1422 tt = gmPathLab.create_measurement_type ( 1423 lab = None, 1424 abbrev = self._PRW_test.GetValue().strip(), 1425 name = self._PRW_test.GetValue().strip(), 1426 unit = gmTools.none_if(self._PRW_units.GetValue().strip(), u'') 1427 ) 1428 pk_type = tt['pk_test_type'] 1429 1430 tr = self.data 1431 1432 tr['pk_episode'] = self._PRW_problem.GetData(can_create=True, is_open=False) 1433 tr['pk_test_type'] = pk_type 1434 tr['pk_intended_reviewer'] = self._PRW_intended_reviewer.GetData() 1435 tr['val_num'] = v_num 1436 tr['val_alpha'] = v_al 1437 tr['val_unit'] = gmTools.coalesce(self._PRW_units.GetData(), self._PRW_units.GetValue()).strip() 1438 tr['clin_when'] = self._DPRW_evaluated.GetData().get_pydt() 1439 1440 ctrls = [ 1441 ('abnormality_indicator', self._PRW_abnormality_indicator), 1442 ('note_test_org', self._TCTRL_note_test_org), 1443 ('comment', self._TCTRL_narrative), 1444 ('val_normal_range', self._TCTRL_normal_range), 1445 ('val_target_range', self._TCTRL_target_range), 1446 ('norm_ref_group', self._TCTRL_norm_ref_group) 1447 ] 1448 for field, widget in ctrls: 1449 tr[field] = widget.GetValue().strip() 1450 1451 ctrls = [ 1452 ('val_normal_min', self._TCTRL_normal_min), 1453 ('val_normal_max', self._TCTRL_normal_max), 1454 ('val_target_min', self._TCTRL_target_min), 1455 ('val_target_max', self._TCTRL_target_max) 1456 ] 1457 for field, widget in ctrls: 1458 val = widget.GetValue().strip() 1459 if val == u'': 1460 tr[field] = None 1461 else: 1462 tr[field] = decimal.Decimal(val.replace(',', u'.', 1)) 1463 1464 tr.save_payload() 1465 1466 if self._CHBOX_review.GetValue() is True: 1467 tr.set_review ( 1468 technically_abnormal = self._CHBOX_abnormal.GetValue(), 1469 clinically_relevant = self._CHBOX_relevant.GetValue(), 1470 comment = gmTools.none_if(self._TCTRL_review_comment.GetValue().strip(), u''), 1471 make_me_responsible = False 1472 ) 1473 1474 return True
1475 #-------------------------------------------------------- 1476 # event handling 1477 #--------------------------------------------------------
1478 - def __register_interests(self):
1479 self._PRW_test.add_callback_on_lose_focus(self._on_leave_test_prw) 1480 self._PRW_abnormality_indicator.add_callback_on_lose_focus(self._on_leave_indicator_prw)
1481 #--------------------------------------------------------
1482 - def _on_leave_test_prw(self):
1483 self.__refresh_loinc_info() 1484 self.__update_units_context()
1485 #--------------------------------------------------------
1486 - def _on_leave_indicator_prw(self):
1487 # if the user hasn't explicitly enabled reviewing 1488 if not self._CHBOX_review.GetValue(): 1489 self._CHBOX_abnormal.SetValue(self._PRW_abnormality_indicator.GetValue().strip() != u'')
1490 #--------------------------------------------------------
1491 - def _on_review_box_checked(self, evt):
1492 self._CHBOX_abnormal.Enable(self._CHBOX_review.GetValue()) 1493 self._CHBOX_relevant.Enable(self._CHBOX_review.GetValue()) 1494 self._TCTRL_review_comment.Enable(self._CHBOX_review.GetValue())
1495 #--------------------------------------------------------
1496 - def _on_test_info_button_pressed(self, event):
1497 1498 pk = self._PRW_test.GetData() 1499 if pk is not None: 1500 tt = gmPathLab.cMeasurementType(aPK_obj = pk) 1501 search_term = u'%s %s %s' % ( 1502 tt['name'], 1503 tt['abbrev'], 1504 gmTools.coalesce(tt['loinc'], u'') 1505 ) 1506 else: 1507 search_term = self._PRW_test.GetValue() 1508 1509 search_term = search_term.replace(' ', u'+') 1510 1511 call_browser_on_measurement_type(measurement_type = search_term)
1512 #-------------------------------------------------------- 1513 # internal helpers 1514 #--------------------------------------------------------
1515 - def __update_units_context(self):
1516 1517 self._PRW_units.unset_context(context = u'loinc') 1518 1519 tt = self._PRW_test.GetData(as_instance = True) 1520 1521 if tt is None: 1522 self._PRW_units.unset_context(context = u'pk_type') 1523 if self._PRW_test.GetValue().strip() == u'': 1524 self._PRW_units.unset_context(context = u'test_name') 1525 else: 1526 self._PRW_units.set_context(context = u'test_name', val = self._PRW_test.GetValue().strip()) 1527 return 1528 1529 self._PRW_units.set_context(context = u'pk_type', val = tt['pk_test_type']) 1530 self._PRW_units.set_context(context = u'test_name', val = tt['name']) 1531 1532 if tt['loinc'] is None: 1533 return 1534 1535 self._PRW_units.set_context(context = u'loinc', val = tt['loinc'])
1536 #--------------------------------------------------------
1537 - def __refresh_loinc_info(self):
1538 1539 self._TCTRL_loinc.SetValue(u'') 1540 1541 if self._PRW_test.GetData() is None: 1542 return 1543 1544 tt = self._PRW_test.GetData(as_instance = True) 1545 1546 if tt['loinc'] is None: 1547 return 1548 1549 info = gmLOINC.loinc2term(loinc = tt['loinc']) 1550 if len(info) == 0: 1551 self._TCTRL_loinc.SetValue(u'') 1552 return 1553 1554 self._TCTRL_loinc.SetValue(u'%s: %s' % (tt['loinc'], info[0]))
1555 #================================================================ 1556 # measurement type handling 1557 #================================================================
1558 -def manage_measurement_types(parent=None):
1559 1560 if parent is None: 1561 parent = wx.GetApp().GetTopWindow() 1562 1563 #------------------------------------------------------------ 1564 def edit(test_type=None): 1565 ea = cMeasurementTypeEAPnl(parent = parent, id = -1, type = test_type) 1566 dlg = gmEditArea.cGenericEditAreaDlg2 ( 1567 parent = parent, 1568 id = -1, 1569 edit_area = ea, 1570 single_entry = gmTools.bool2subst((test_type is None), False, True) 1571 ) 1572 dlg.SetTitle(gmTools.coalesce(test_type, _('Adding measurement type'), _('Editing measurement type'))) 1573 1574 if dlg.ShowModal() == wx.ID_OK: 1575 dlg.Destroy() 1576 return True 1577 1578 dlg.Destroy() 1579 return False
1580 #------------------------------------------------------------ 1581 def refresh(lctrl): 1582 mtypes = gmPathLab.get_measurement_types(order_by = 'name, abbrev') 1583 items = [ [ 1584 m['abbrev'], 1585 m['name'], 1586 gmTools.coalesce(m['loinc'], u''), 1587 gmTools.coalesce(m['conversion_unit'], u''), 1588 gmTools.coalesce(m['comment_type'], u''), 1589 gmTools.coalesce(m['internal_name_org'], _('in-house')), 1590 gmTools.coalesce(m['comment_org'], u''), 1591 m['pk_test_type'] 1592 ] for m in mtypes ] 1593 lctrl.set_string_items(items) 1594 lctrl.set_data(mtypes) 1595 #------------------------------------------------------------ 1596 def delete(measurement_type): 1597 if measurement_type.in_use: 1598 gmDispatcher.send ( 1599 signal = 'statustext', 1600 beep = True, 1601 msg = _('Cannot delete measurement type [%s (%s)] because it is in use.') % (measurement_type['name'], measurement_type['abbrev']) 1602 ) 1603 return False 1604 gmPathLab.delete_measurement_type(measurement_type = measurement_type['pk_test_type']) 1605 return True 1606 #------------------------------------------------------------ 1607 msg = _( 1608 '\n' 1609 'These are the measurement types currently defined in GNUmed.\n' 1610 '\n' 1611 ) 1612 1613 gmListWidgets.get_choices_from_list ( 1614 parent = parent, 1615 msg = msg, 1616 caption = _('Showing measurement types.'), 1617 columns = [_('Abbrev'), _('Name'), _('LOINC'), _('Base unit'), _('Comment'), _('Org'), _('Comment'), u'#'], 1618 single_selection = True, 1619 refresh_callback = refresh, 1620 edit_callback = edit, 1621 new_callback = edit, 1622 delete_callback = delete 1623 ) 1624 #----------------------------------------------------------------
1625 -class cMeasurementTypePhraseWheel(gmPhraseWheel.cPhraseWheel):
1626
1627 - def __init__(self, *args, **kwargs):
1628 1629 query = u""" 1630 SELECT DISTINCT ON (field_label) 1631 pk_test_type AS data, 1632 name_tt 1633 || ' (' 1634 || coalesce ( 1635 (SELECT internal_name FROM clin.test_org cto WHERE cto.pk = vcutt.pk_test_org), 1636 '%(in_house)s' 1637 ) 1638 || ')' 1639 AS field_label, 1640 name_tt 1641 || ' (' 1642 || code_tt || ', ' 1643 || abbrev_tt || ', ' 1644 || coalesce(abbrev_meta || ': ' || name_meta || ', ', '') 1645 || coalesce ( 1646 (SELECT internal_name FROM clin.test_org cto WHERE cto.pk = vcutt.pk_test_org), 1647 '%(in_house)s' 1648 ) 1649 || ')' 1650 AS list_label 1651 FROM 1652 clin.v_unified_test_types vcutt 1653 WHERE 1654 abbrev_meta %%(fragment_condition)s 1655 OR 1656 name_meta %%(fragment_condition)s 1657 OR 1658 abbrev_tt %%(fragment_condition)s 1659 OR 1660 name_tt %%(fragment_condition)s 1661 OR 1662 code_tt %%(fragment_condition)s 1663 ORDER BY field_label 1664 LIMIT 50""" % {'in_house': _('generic / in house lab')} 1665 1666 mp = gmMatchProvider.cMatchProvider_SQL2(queries=query) 1667 mp.setThresholds(1, 2, 4) 1668 mp.word_separators = '[ \t:@]+' 1669 gmPhraseWheel.cPhraseWheel.__init__(self, *args, **kwargs) 1670 self.matcher = mp 1671 self.SetToolTipString(_('Select the type of measurement.')) 1672 self.selection_only = False
1673 #------------------------------------------------------------
1674 - def _data2instance(self):
1675 if self.GetData() is None: 1676 return None 1677 1678 return gmPathLab.cMeasurementType(aPK_obj = self.GetData())
1679 #----------------------------------------------------------------
1680 -class cMeasurementOrgPhraseWheel(gmPhraseWheel.cPhraseWheel):
1681
1682 - def __init__(self, *args, **kwargs):
1683 1684 query = u""" 1685 select distinct on (internal_name) 1686 pk, 1687 internal_name 1688 from clin.test_org 1689 where 1690 internal_name %(fragment_condition)s 1691 order by internal_name 1692 limit 50""" 1693 mp = gmMatchProvider.cMatchProvider_SQL2(queries=query) 1694 mp.setThresholds(1, 2, 4) 1695 #mp.word_separators = '[ \t:@]+' 1696 gmPhraseWheel.cPhraseWheel.__init__(self, *args, **kwargs) 1697 self.matcher = mp 1698 self.SetToolTipString(_('The name of the path lab/diagnostic organisation.')) 1699 self.selection_only = False
1700 #------------------------------------------------------------
1701 - def _create_data(self):
1702 if self.data is not None: 1703 _log.debug('data already set, not creating') 1704 return 1705 1706 if self.GetValue().strip() == u'': 1707 _log.debug('cannot create new lab, missing name') 1708 return 1709 1710 lab = gmPathLab.create_test_org(name = self.GetValue().strip()) 1711 self.SetText(value = lab['internal_name'], data = lab['pk']) 1712 return
1713 #------------------------------------------------------------
1714 - def _data2instance(self):
1715 return gmPathLab.cTestOrg(aPK_obj = self.GetData())
1716 #---------------------------------------------------------------- 1717 from Gnumed.wxGladeWidgets import wxgMeasurementTypeEAPnl 1718
1719 -class cMeasurementTypeEAPnl(wxgMeasurementTypeEAPnl.wxgMeasurementTypeEAPnl, gmEditArea.cGenericEditAreaMixin):
1720
1721 - def __init__(self, *args, **kwargs):
1722 1723 try: 1724 data = kwargs['type'] 1725 del kwargs['type'] 1726 except KeyError: 1727 data = None 1728 1729 wxgMeasurementTypeEAPnl.wxgMeasurementTypeEAPnl.__init__(self, *args, **kwargs) 1730 gmEditArea.cGenericEditAreaMixin.__init__(self) 1731 self.mode = 'new' 1732 self.data = data 1733 if data is not None: 1734 self.mode = 'edit' 1735 1736 self.__init_ui()
1737 1738 #----------------------------------------------------------------
1739 - def __init_ui(self):
1740 1741 # name phraseweel 1742 query = u""" 1743 select distinct on (name) 1744 pk, 1745 name 1746 from clin.test_type 1747 where 1748 name %(fragment_condition)s 1749 order by name 1750 limit 50""" 1751 mp = gmMatchProvider.cMatchProvider_SQL2(queries=query) 1752 mp.setThresholds(1, 2, 4) 1753 self._PRW_name.matcher = mp 1754 self._PRW_name.selection_only = False 1755 self._PRW_name.add_callback_on_lose_focus(callback = self._on_name_lost_focus) 1756 1757 # abbreviation 1758 query = u""" 1759 select distinct on (abbrev) 1760 pk, 1761 abbrev 1762 from clin.test_type 1763 where 1764 abbrev %(fragment_condition)s 1765 order by abbrev 1766 limit 50""" 1767 mp = gmMatchProvider.cMatchProvider_SQL2(queries=query) 1768 mp.setThresholds(1, 2, 3) 1769 self._PRW_abbrev.matcher = mp 1770 self._PRW_abbrev.selection_only = False 1771 1772 # unit 1773 self._PRW_conversion_unit.selection_only = False 1774 1775 # loinc 1776 query = u""" 1777 SELECT DISTINCT ON (list_label) 1778 data, 1779 field_label, 1780 list_label 1781 FROM (( 1782 1783 SELECT 1784 loinc AS data, 1785 loinc AS field_label, 1786 (loinc || ': ' || abbrev || ' (' || name || ')') AS list_label 1787 FROM clin.test_type 1788 WHERE loinc %(fragment_condition)s 1789 LIMIT 50 1790 1791 ) UNION ALL ( 1792 1793 SELECT 1794 code AS data, 1795 code AS field_label, 1796 (code || ': ' || term) AS list_label 1797 FROM ref.v_coded_terms 1798 WHERE 1799 coding_system = 'LOINC' 1800 AND 1801 lang = i18n.get_curr_lang() 1802 AND 1803 (code %(fragment_condition)s 1804 OR 1805 term %(fragment_condition)s) 1806 LIMIT 50 1807 1808 ) UNION ALL ( 1809 1810 SELECT 1811 code AS data, 1812 code AS field_label, 1813 (code || ': ' || term) AS list_label 1814 FROM ref.v_coded_terms 1815 WHERE 1816 coding_system = 'LOINC' 1817 AND 1818 lang = 'en_EN' 1819 AND 1820 (code %(fragment_condition)s 1821 OR 1822 term %(fragment_condition)s) 1823 LIMIT 50 1824 1825 ) UNION ALL ( 1826 1827 SELECT 1828 code AS data, 1829 code AS field_label, 1830 (code || ': ' || term) AS list_label 1831 FROM ref.v_coded_terms 1832 WHERE 1833 coding_system = 'LOINC' 1834 AND 1835 (code %(fragment_condition)s 1836 OR 1837 term %(fragment_condition)s) 1838 LIMIT 50 1839 ) 1840 ) AS all_known_loinc 1841 1842 ORDER BY list_label 1843 LIMIT 50""" 1844 mp = gmMatchProvider.cMatchProvider_SQL2(queries = query) 1845 mp.setThresholds(1, 2, 4) 1846 self._PRW_loinc.matcher = mp 1847 self._PRW_loinc.selection_only = False 1848 self._PRW_loinc.add_callback_on_lose_focus(callback = self._on_loinc_lost_focus)
1849 #----------------------------------------------------------------
1850 - def _on_name_lost_focus(self):
1851 1852 test = self._PRW_name.GetValue().strip() 1853 1854 if test == u'': 1855 self._PRW_conversion_unit.unset_context(context = u'test_name') 1856 return 1857 1858 self._PRW_conversion_unit.set_context(context = u'test_name', val = test)
1859 #----------------------------------------------------------------
1860 - def _on_loinc_lost_focus(self):
1861 loinc = self._PRW_loinc.GetData() 1862 1863 if loinc is None: 1864 self._TCTRL_loinc_info.SetValue(u'') 1865 self._PRW_conversion_unit.unset_context(context = u'loinc') 1866 return 1867 1868 self._PRW_conversion_unit.set_context(context = u'loinc', val = loinc) 1869 1870 info = gmLOINC.loinc2term(loinc = loinc) 1871 if len(info) == 0: 1872 self._TCTRL_loinc_info.SetValue(u'') 1873 return 1874 1875 self._TCTRL_loinc_info.SetValue(info[0])
1876 #---------------------------------------------------------------- 1877 # generic Edit Area mixin API 1878 #----------------------------------------------------------------
1879 - def _valid_for_save(self):
1880 1881 has_errors = False 1882 for field in [self._PRW_name, self._PRW_abbrev, self._PRW_conversion_unit]: 1883 if field.GetValue().strip() in [u'', None]: 1884 has_errors = True 1885 field.display_as_valid(valid = False) 1886 else: 1887 field.display_as_valid(valid = True) 1888 field.Refresh() 1889 1890 return (not has_errors)
1891 #----------------------------------------------------------------
1892 - def _save_as_new(self):
1893 1894 pk_org = self._PRW_test_org.GetData() 1895 if pk_org is None: 1896 pk_org = gmPathLab.create_measurement_org ( 1897 name = gmTools.none_if(self._PRW_test_org.GetValue().strip(), u''), 1898 comment = gmTools.none_if(self._TCTRL_comment_org.GetValue().strip(), u'') 1899 ) 1900 1901 tt = gmPathLab.create_measurement_type ( 1902 lab = pk_org, 1903 abbrev = self._PRW_abbrev.GetValue().strip(), 1904 name = self._PRW_name.GetValue().strip(), 1905 unit = gmTools.coalesce ( 1906 self._PRW_conversion_unit.GetData(), 1907 self._PRW_conversion_unit.GetValue() 1908 ).strip() 1909 ) 1910 if self._PRW_loinc.GetData() is not None: 1911 tt['loinc'] = gmTools.none_if(self._PRW_loinc.GetData().strip(), u'') 1912 else: 1913 tt['loinc'] = gmTools.none_if(self._PRW_loinc.GetValue().strip(), u'') 1914 tt['comment_type'] = gmTools.none_if(self._TCTRL_comment_type.GetValue().strip(), u'') 1915 tt.save() 1916 1917 self.data = tt 1918 1919 return True
1920 #----------------------------------------------------------------
1921 - def _save_as_update(self):
1922 1923 pk_org = self._PRW_test_org.GetData() 1924 if pk_org is None: 1925 pk_org = gmPathLab.create_measurement_org ( 1926 name = gmTools.none_if(self._PRW_test_org.GetValue().strip(), u''), 1927 comment = gmTools.none_if(self._TCTRL_comment_org.GetValue().strip(), u'') 1928 ) 1929 1930 self.data['pk_test_org'] = pk_org 1931 self.data['abbrev'] = self._PRW_abbrev.GetValue().strip() 1932 self.data['name'] = self._PRW_name.GetValue().strip() 1933 self.data['conversion_unit'] = gmTools.coalesce ( 1934 self._PRW_conversion_unit.GetData(), 1935 self._PRW_conversion_unit.GetValue() 1936 ).strip() 1937 if self._PRW_loinc.GetData() is not None: 1938 self.data['loinc'] = gmTools.none_if(self._PRW_loinc.GetData().strip(), u'') 1939 if self._PRW_loinc.GetData() is not None: 1940 self.data['loinc'] = gmTools.none_if(self._PRW_loinc.GetData().strip(), u'') 1941 else: 1942 self.data['loinc'] = gmTools.none_if(self._PRW_loinc.GetValue().strip(), u'') 1943 self.data['comment_type'] = gmTools.none_if(self._TCTRL_comment_type.GetValue().strip(), u'') 1944 self.data.save() 1945 1946 return True
1947 #----------------------------------------------------------------
1948 - def _refresh_as_new(self):
1949 self._PRW_name.SetText(u'', None, True) 1950 self._on_name_lost_focus() 1951 self._PRW_abbrev.SetText(u'', None, True) 1952 self._PRW_conversion_unit.SetText(u'', None, True) 1953 self._PRW_loinc.SetText(u'', None, True) 1954 self._on_loinc_lost_focus() 1955 self._TCTRL_comment_type.SetValue(u'') 1956 self._PRW_test_org.SetText(u'', None, True) 1957 self._TCTRL_comment_org.SetValue(u'') 1958 1959 self._PRW_name.SetFocus()
1960 #----------------------------------------------------------------
1961 - def _refresh_from_existing(self):
1962 self._PRW_name.SetText(self.data['name'], self.data['name'], True) 1963 self._on_name_lost_focus() 1964 self._PRW_abbrev.SetText(self.data['abbrev'], self.data['abbrev'], True) 1965 self._PRW_conversion_unit.SetText ( 1966 gmTools.coalesce(self.data['conversion_unit'], u''), 1967 self.data['conversion_unit'], 1968 True 1969 ) 1970 self._PRW_loinc.SetText ( 1971 gmTools.coalesce(self.data['loinc'], u''), 1972 self.data['loinc'], 1973 True 1974 ) 1975 self._on_loinc_lost_focus() 1976 self._TCTRL_comment_type.SetValue(gmTools.coalesce(self.data['comment_type'], u'')) 1977 self._PRW_test_org.SetText ( 1978 gmTools.coalesce(self.data['pk_test_org'], u'', self.data['internal_name_org']), 1979 self.data['pk_test_org'], 1980 True 1981 ) 1982 self._TCTRL_comment_org.SetValue(gmTools.coalesce(self.data['comment_org'], u'')) 1983 1984 self._PRW_name.SetFocus()
1985 #----------------------------------------------------------------
1987 self._refresh_as_new() 1988 self._PRW_test_org.SetText ( 1989 gmTools.coalesce(self.data['pk_test_org'], u'', self.data['internal_name_org']), 1990 self.data['pk_test_org'], 1991 True 1992 ) 1993 self._TCTRL_comment_org.SetValue(gmTools.coalesce(self.data['comment_org'], u'')) 1994 1995 self._PRW_name.SetFocus()
1996 #================================================================ 1997 _SQL_units_from_test_results = u""" 1998 -- via clin.v_test_results.pk_type (for types already used in results) 1999 SELECT 2000 val_unit AS data, 2001 val_unit AS field_label, 2002 val_unit || ' (' || name_tt || ')' AS list_label, 2003 1 AS rank 2004 FROM 2005 clin.v_test_results 2006 WHERE 2007 ( 2008 val_unit %(fragment_condition)s 2009 OR 2010 conversion_unit %(fragment_condition)s 2011 ) 2012 %(ctxt_type_pk)s 2013 %(ctxt_test_name)s 2014 """ 2015 2016 _SQL_units_from_test_types = u""" 2017 -- via clin.test_type (for types not yet used in results) 2018 SELECT 2019 conversion_unit AS data, 2020 conversion_unit AS field_label, 2021 conversion_unit || ' (' || name || ')' AS list_label, 2022 2 AS rank 2023 FROM 2024 clin.test_type 2025 WHERE 2026 conversion_unit %(fragment_condition)s 2027 %(ctxt_ctt)s 2028 """ 2029 2030 _SQL_units_from_loinc_ipcc = u""" 2031 -- via ref.loinc.ipcc_units 2032 SELECT 2033 ipcc_units AS data, 2034 ipcc_units AS field_label, 2035 ipcc_units || ' (LOINC.ipcc: ' || term || ')' AS list_label, 2036 3 AS rank 2037 FROM 2038 ref.loinc 2039 WHERE 2040 ipcc_units %(fragment_condition)s 2041 %(ctxt_loinc)s 2042 %(ctxt_loinc_term)s 2043 """ 2044 2045 _SQL_units_from_loinc_submitted = u""" 2046 -- via ref.loinc.submitted_units 2047 SELECT 2048 submitted_units AS data, 2049 submitted_units AS field_label, 2050 submitted_units || ' (LOINC.submitted:' || term || ')' AS list_label, 2051 3 AS rank 2052 FROM 2053 ref.loinc 2054 WHERE 2055 submitted_units %(fragment_condition)s 2056 %(ctxt_loinc)s 2057 %(ctxt_loinc_term)s 2058 """ 2059 2060 _SQL_units_from_loinc_example = u""" 2061 -- via ref.loinc.example_units 2062 SELECT 2063 example_units AS data, 2064 example_units AS field_label, 2065 example_units || ' (LOINC.example: ' || term || ')' AS list_label, 2066 3 AS rank 2067 FROM 2068 ref.loinc 2069 WHERE 2070 example_units %(fragment_condition)s 2071 %(ctxt_loinc)s 2072 %(ctxt_loinc_term)s 2073 """ 2074 2075 _SQL_units_from_atc = u""" 2076 -- via rev.atc.unit 2077 SELECT 2078 unit AS data, 2079 unit AS field_label, 2080 unit || ' (ATC: ' || term || ')' AS list_label, 2081 2 AS rank 2082 FROM 2083 ref.atc 2084 WHERE 2085 unit IS NOT NULL 2086 AND 2087 unit %(fragment_condition)s 2088 """ 2089 2090 _SQL_units_from_consumable_substance = u""" 2091 -- via ref.consumable_substance.unit 2092 SELECT 2093 unit AS data, 2094 unit AS field_label, 2095 unit || ' (' || description || ')' AS list_label, 2096 2 AS rank 2097 FROM 2098 ref.consumable_substance 2099 WHERE 2100 unit %(fragment_condition)s 2101 %(ctxt_substance)s 2102 """ 2103 #================================================================
2104 -class cUnitPhraseWheel(gmPhraseWheel.cPhraseWheel):
2105
2106 - def __init__(self, *args, **kwargs):
2107 2108 query = u""" 2109 SELECT DISTINCT ON (data) 2110 data, 2111 field_label, 2112 list_label 2113 FROM ( 2114 2115 SELECT 2116 data, 2117 field_label, 2118 list_label, 2119 rank 2120 FROM ( 2121 (%s) UNION ALL 2122 (%s) UNION ALL 2123 (%s) UNION ALL 2124 (%s) UNION ALL 2125 (%s) UNION ALL 2126 (%s) UNION ALL 2127 (%s) 2128 ) AS all_matching_units 2129 WHERE data IS NOT NULL 2130 ORDER BY rank 2131 2132 ) AS ranked_matching_units 2133 LIMIT 50""" % ( 2134 _SQL_units_from_test_results, 2135 _SQL_units_from_test_types, 2136 _SQL_units_from_loinc_ipcc, 2137 _SQL_units_from_loinc_submitted, 2138 _SQL_units_from_loinc_example, 2139 _SQL_units_from_atc, 2140 _SQL_units_from_consumable_substance 2141 ) 2142 2143 ctxt = { 2144 'ctxt_type_pk': { 2145 'where_part': u'AND pk_test_type = %(pk_type)s', 2146 'placeholder': u'pk_type' 2147 }, 2148 'ctxt_test_name': { 2149 'where_part': u'AND %(test_name)s IN (name_tt, name_meta, code_tt, abbrev_meta)', 2150 'placeholder': u'test_name' 2151 }, 2152 'ctxt_ctt': { 2153 'where_part': u'AND %(test_name)s IN (name, code, abbrev)', 2154 'placeholder': u'test_name' 2155 }, 2156 'ctxt_loinc': { 2157 'where_part': u'AND code = %(loinc)s', 2158 'placeholder': u'loinc' 2159 }, 2160 'ctxt_loinc_term': { 2161 'where_part': u'AND term ~* %(test_name)s', 2162 'placeholder': u'test_name' 2163 }, 2164 'ctxt_substance': { 2165 'where_part': u'AND description ~* %(substance)s', 2166 'placeholder': u'substance' 2167 } 2168 } 2169 2170 mp = gmMatchProvider.cMatchProvider_SQL2(queries = query, context = ctxt) 2171 mp.setThresholds(1, 2, 4) 2172 #mp.print_queries = True 2173 gmPhraseWheel.cPhraseWheel.__init__(self, *args, **kwargs) 2174 self.matcher = mp 2175 self.SetToolTipString(_('Select the desired unit for the amount or measurement.')) 2176 self.selection_only = False 2177 self.phrase_separators = u'[;|]+'
2178 #================================================================ 2179 2180 #================================================================
2181 -class cTestResultIndicatorPhraseWheel(gmPhraseWheel.cPhraseWheel):
2182
2183 - def __init__(self, *args, **kwargs):
2184 2185 query = u""" 2186 select distinct abnormality_indicator, 2187 abnormality_indicator, abnormality_indicator 2188 from clin.v_test_results 2189 where 2190 abnormality_indicator %(fragment_condition)s 2191 order by abnormality_indicator 2192 limit 25""" 2193 2194 mp = gmMatchProvider.cMatchProvider_SQL2(queries=query) 2195 mp.setThresholds(1, 1, 2) 2196 mp.ignored_chars = "[.'\\\[\]#$%_]+" + '"' 2197 mp.word_separators = '[ \t&:]+' 2198 gmPhraseWheel.cPhraseWheel.__init__ ( 2199 self, 2200 *args, 2201 **kwargs 2202 ) 2203 self.matcher = mp 2204 self.SetToolTipString(_('Select an indicator for the level of abnormality.')) 2205 self.selection_only = False
2206 #================================================================ 2207 # measurement org widgets / functions 2208 #----------------------------------------------------------------
2209 -def edit_measurement_org(parent=None, org=None):
2210 ea = cMeasurementOrgEAPnl(parent = parent, id = -1) 2211 ea.data = org 2212 ea.mode = gmTools.coalesce(org, 'new', 'edit') 2213 dlg = gmEditArea.cGenericEditAreaDlg2(parent = parent, id = -1, edit_area = ea) 2214 dlg.SetTitle(gmTools.coalesce(org, _('Adding new diagnostic org'), _('Editing diagnostic org'))) 2215 if dlg.ShowModal() == wx.ID_OK: 2216 dlg.Destroy() 2217 return True 2218 dlg.Destroy() 2219 return False
2220 #----------------------------------------------------------------
2221 -def manage_measurement_orgs(parent=None):
2222 2223 if parent is None: 2224 parent = wx.GetApp().GetTopWindow() 2225 2226 #------------------------------------------------------------ 2227 def edit(org=None): 2228 return edit_measurement_org(parent = parent, org = org)
2229 #------------------------------------------------------------ 2230 def refresh(lctrl): 2231 orgs = gmPathLab.get_test_orgs() 2232 lctrl.set_string_items ([ 2233 (o['internal_name'], gmTools.coalesce(o['contact'], u''), gmTools.coalesce(o['comment']), o['pk']) 2234 for o in orgs 2235 ]) 2236 lctrl.set_data(orgs) 2237 #------------------------------------------------------------ 2238 def delete(measurement_type): 2239 if measurement_type.in_use: 2240 gmDispatcher.send ( 2241 signal = 'statustext', 2242 beep = True, 2243 msg = _('Cannot delete measurement type [%s (%s)] because it is in use.') % (measurement_type['name'], measurement_type['abbrev']) 2244 ) 2245 return False 2246 gmPathLab.delete_measurement_type(measurement_type = measurement_type['pk_test_type']) 2247 return True 2248 #------------------------------------------------------------ 2249 gmListWidgets.get_choices_from_list ( 2250 parent = parent, 2251 msg = _('\nThese are the diagnostic orgs (path labs etc) currently defined in GNUmed.\n\n'), 2252 caption = _('Showing diagnostic orgs.'), 2253 columns = [_('Name'), _('Contact'), _('Comment'), u'#'], 2254 single_selection = True, 2255 refresh_callback = refresh, 2256 edit_callback = edit, 2257 new_callback = edit 2258 # ,delete_callback = delete 2259 ) 2260 2261 2262 #---------------------------------------------------------------- 2263 from Gnumed.wxGladeWidgets import wxgMeasurementOrgEAPnl 2264
2265 -class cMeasurementOrgEAPnl(wxgMeasurementOrgEAPnl.wxgMeasurementOrgEAPnl, gmEditArea.cGenericEditAreaMixin):
2266
2267 - def __init__(self, *args, **kwargs):
2268 2269 try: 2270 data = kwargs['org'] 2271 del kwargs['org'] 2272 except KeyError: 2273 data = None 2274 2275 wxgMeasurementOrgEAPnl.wxgMeasurementOrgEAPnl.__init__(self, *args, **kwargs) 2276 gmEditArea.cGenericEditAreaMixin.__init__(self) 2277 2278 # Code using this mixin should set mode and data 2279 # after instantiating the class: 2280 self.mode = 'new' 2281 self.data = data 2282 if data is not None: 2283 self.mode = 'edit'
2284 2285 #self.__init_ui() 2286 #---------------------------------------------------------------- 2287 # def __init_ui(self): 2288 # # adjust phrasewheels etc 2289 #---------------------------------------------------------------- 2290 # generic Edit Area mixin API 2291 #----------------------------------------------------------------
2292 - def _valid_for_save(self):
2293 has_errors = False 2294 if self._PRW_name.GetValue().strip() == u'': 2295 has_errors = True 2296 self._PRW_name.display_as_valid(valid = False) 2297 else: 2298 self._PRW_name.display_as_valid(valid = True) 2299 2300 return (not has_errors)
2301 #----------------------------------------------------------------
2302 - def _save_as_new(self):
2303 # save the data as a new instance 2304 data = self._PRW_name.GetData(can_create = True, as_instance = True) 2305 2306 data['contact'] = self._TCTRL_contact.GetValue().strip() 2307 data['comment'] = self._TCTRL_comment.GetValue().strip() 2308 data.save() 2309 2310 # must be done very late or else the property access 2311 # will refresh the display such that later field 2312 # access will return empty values 2313 self.data = data 2314 2315 return True
2316 #----------------------------------------------------------------
2317 - def _save_as_update(self):
2318 self.data['internal_name'] = self._PRW_name.GetValue().strip() 2319 self.data['contact'] = self._TCTRL_contact.GetValue().strip() 2320 self.data['comment'] = self._TCTRL_comment.GetValue().strip() 2321 self.data.save() 2322 return True
2323 #----------------------------------------------------------------
2324 - def _refresh_as_new(self):
2325 self._PRW_name.SetText(value = u'', data = None) 2326 self._TCTRL_contact.SetValue(u'') 2327 self._TCTRL_comment.SetValue(u'')
2328 #----------------------------------------------------------------
2329 - def _refresh_from_existing(self):
2330 self._PRW_name.SetText(value = self.data['internal_name'], data = self.data['pk']) 2331 self._TCTRL_contact.SetValue(gmTools.coalesce(self.data['contact'], u'')) 2332 self._TCTRL_comment.SetValue(gmTools.coalesce(self.data['comment'], u''))
2333 #----------------------------------------------------------------
2335 self._refresh_as_new()
2336 #================================================================
2337 -def manage_meta_test_types(parent=None):
2338 2339 if parent is None: 2340 parent = wx.GetApp().GetTopWindow() 2341 2342 msg = _( 2343 '\n' 2344 'These are the meta test types currently defined in GNUmed.\n' 2345 '\n' 2346 'Meta test types allow you to aggregate several actual test types used\n' 2347 'by pathology labs into one logical type.\n' 2348 '\n' 2349 'This is useful for grouping together results of tests which come under\n' 2350 'different names but really are the same thing. This often happens when\n' 2351 'you switch labs or the lab starts using another test method.\n' 2352 ) 2353 2354 mtts = gmPathLab.get_meta_test_types() 2355 2356 gmListWidgets.get_choices_from_list ( 2357 parent = parent, 2358 msg = msg, 2359 caption = _('Showing meta test types.'), 2360 columns = [_('Abbrev'), _('Name'), _('LOINC'), _('Comment'), u'#'], 2361 choices = [ [ 2362 m['abbrev'], 2363 m['name'], 2364 gmTools.coalesce(m['loinc'], u''), 2365 gmTools.coalesce(m['comment'], u''), 2366 m['pk'] 2367 ] for m in mtts ], 2368 data = mtts, 2369 single_selection = True, 2370 #edit_callback = edit, 2371 #new_callback = edit, 2372 #delete_callback = delete, 2373 #refresh_callback = refresh 2374 )
2375 #================================================================ 2376 # main 2377 #---------------------------------------------------------------- 2378 if __name__ == '__main__': 2379 2380 from Gnumed.pycommon import gmLog2 2381 2382 gmI18N.activate_locale() 2383 gmI18N.install_domain() 2384 gmDateTime.init() 2385 2386 #------------------------------------------------------------
2387 - def test_grid():
2388 pat = gmPersonSearch.ask_for_patient() 2389 app = wx.PyWidgetTester(size = (500, 300)) 2390 lab_grid = cMeasurementsGrid(parent = app.frame, id = -1) 2391 lab_grid.patient = pat 2392 app.frame.Show() 2393 app.MainLoop()
2394 #------------------------------------------------------------
2395 - def test_test_ea_pnl():
2396 pat = gmPersonSearch.ask_for_patient() 2397 gmPatSearchWidgets.set_active_patient(patient=pat) 2398 app = wx.PyWidgetTester(size = (500, 300)) 2399 ea = cMeasurementEditAreaPnl(parent = app.frame, id = -1) 2400 app.frame.Show() 2401 app.MainLoop()
2402 #------------------------------------------------------------ 2403 # def test_primary_care_vitals_pnl(): 2404 # app = wx.PyWidgetTester(size = (500, 300)) 2405 # pnl = wxgPrimaryCareVitalsInputPnl.wxgPrimaryCareVitalsInputPnl(parent = app.frame, id = -1) 2406 # app.frame.Show() 2407 # app.MainLoop() 2408 #------------------------------------------------------------ 2409 if (len(sys.argv) > 1) and (sys.argv[1] == 'test'): 2410 #test_grid() 2411 test_test_ea_pnl() 2412 #test_primary_care_vitals_pnl() 2413 2414 #================================================================ 2415