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

Source Code for Module Gnumed.wxpython.gmDocumentWidgets

   1  # -*- coding: utf-8 -*- 
   2  #============================================================ 
   3   
   4   
   5  __doc__ = """GNUmed medical document handling widgets.""" 
   6   
   7  __license__ = "GPL v2 or later" 
   8  __author__ = "Karsten Hilbert <Karsten.Hilbert@gmx.net>" 
   9   
  10  #============================================================ 
  11  import os.path 
  12  import os 
  13  import sys 
  14  import re as regex 
  15  import logging 
  16  import datetime as pydt 
  17   
  18   
  19  import wx 
  20  import wx.lib.mixins.treemixin as treemixin 
  21   
  22   
  23  if __name__ == '__main__': 
  24          sys.path.insert(0, '../../') 
  25  from Gnumed.pycommon import gmI18N 
  26  if __name__ == '__main__': 
  27          gmI18N.activate_locale() 
  28          gmI18N.install_domain(domain = 'gnumed') 
  29  from Gnumed.pycommon import gmCfg 
  30  from Gnumed.pycommon import gmCfg2 
  31  from Gnumed.pycommon import gmPG2 
  32  from Gnumed.pycommon import gmMimeLib 
  33  from Gnumed.pycommon import gmMatchProvider 
  34  from Gnumed.pycommon import gmDispatcher 
  35  from Gnumed.pycommon import gmDateTime 
  36  from Gnumed.pycommon import gmTools 
  37  from Gnumed.pycommon import gmShellAPI 
  38  from Gnumed.pycommon import gmHooks 
  39  from Gnumed.pycommon import gmNetworkTools 
  40  from Gnumed.pycommon import gmMimeLib 
  41   
  42  from Gnumed.business import gmPerson 
  43  from Gnumed.business import gmStaff 
  44  from Gnumed.business import gmDocuments 
  45  from Gnumed.business import gmEMRStructItems 
  46  from Gnumed.business import gmPraxis 
  47  from Gnumed.business import gmDICOM 
  48  from Gnumed.business import gmProviderInbox 
  49  from Gnumed.business import gmOrganization 
  50   
  51  from Gnumed.wxpython import gmGuiHelpers 
  52  from Gnumed.wxpython import gmRegetMixin 
  53  from Gnumed.wxpython import gmPhraseWheel 
  54  from Gnumed.wxpython import gmPlugin 
  55  from Gnumed.wxpython import gmEncounterWidgets 
  56  from Gnumed.wxpython import gmListWidgets 
  57  from Gnumed.wxpython import gmRegetMixin 
  58   
  59   
  60  _log = logging.getLogger('gm.ui') 
  61   
  62   
  63  default_chunksize = 1 * 1024 * 1024             # 1 MB 
  64   
  65  #============================================================ 
66 -def manage_document_descriptions(parent=None, document=None):
67 68 #----------------------------------- 69 def delete_item(item): 70 doit = gmGuiHelpers.gm_show_question ( 71 _( 'Are you sure you want to delete this\n' 72 'description from the document ?\n' 73 ), 74 _('Deleting document description') 75 ) 76 if not doit: 77 return True 78 79 document.delete_description(pk = item[0]) 80 return True
81 #----------------------------------- 82 def add_item(): 83 dlg = gmGuiHelpers.cMultilineTextEntryDlg ( 84 parent, 85 -1, 86 title = _('Adding document description'), 87 msg = _('Below you can add a document description.\n') 88 ) 89 result = dlg.ShowModal() 90 if result == wx.ID_SAVE: 91 document.add_description(dlg.value) 92 93 dlg.DestroyLater() 94 return True 95 #----------------------------------- 96 def edit_item(item): 97 dlg = gmGuiHelpers.cMultilineTextEntryDlg ( 98 parent, 99 -1, 100 title = _('Editing document description'), 101 msg = _('Below you can edit the document description.\n'), 102 text = item[1] 103 ) 104 result = dlg.ShowModal() 105 if result == wx.ID_SAVE: 106 document.update_description(pk = item[0], description = dlg.value) 107 108 dlg.DestroyLater() 109 return True 110 #----------------------------------- 111 def refresh_list(lctrl): 112 descriptions = document.get_descriptions() 113 114 lctrl.set_string_items(items = [ 115 '%s%s' % ( (' '.join(regex.split('\r\n+|\r+|\n+|\t+', desc[1])))[:30], gmTools.u_ellipsis ) 116 for desc in descriptions 117 ]) 118 lctrl.set_data(data = descriptions) 119 #----------------------------------- 120 121 gmListWidgets.get_choices_from_list ( 122 parent = parent, 123 msg = _('Select the description you are interested in.\n'), 124 caption = _('Managing document descriptions'), 125 columns = [_('Description')], 126 edit_callback = edit_item, 127 new_callback = add_item, 128 delete_callback = delete_item, 129 refresh_callback = refresh_list, 130 single_selection = True, 131 can_return_empty = True 132 ) 133 134 return True 135 136 #============================================================
137 -def _save_file_as_new_document(**kwargs):
138 try: 139 del kwargs['signal'] 140 del kwargs['sender'] 141 except KeyError: 142 pass 143 wx.CallAfter(save_file_as_new_document, **kwargs)
144
145 -def _save_files_as_new_document(**kwargs):
146 try: 147 del kwargs['signal'] 148 del kwargs['sender'] 149 except KeyError: 150 pass 151 wx.CallAfter(save_files_as_new_document, **kwargs)
152 #----------------------
153 -def save_file_as_new_document(parent=None, filename=None, document_type=None, unlock_patient=False, episode=None, review_as_normal=False, pk_org_unit=None):
154 return save_files_as_new_document ( 155 parent = parent, 156 filenames = [filename], 157 document_type = document_type, 158 unlock_patient = unlock_patient, 159 episode = episode, 160 review_as_normal = review_as_normal, 161 pk_org_unit = pk_org_unit 162 )
163 164 #----------------------
165 -def save_files_as_new_document(parent=None, filenames=None, document_type=None, unlock_patient=False, episode=None, review_as_normal=False, reference=None, pk_org_unit=None, date_generated=None, comment=None, reviewer=None, pk_document_type=None):
166 167 pat = gmPerson.gmCurrentPatient() 168 if not pat.connected: 169 return None 170 171 emr = pat.emr 172 173 if parent is None: 174 parent = wx.GetApp().GetTopWindow() 175 176 if episode is None: 177 all_epis = emr.get_episodes() 178 # FIXME: what to do here ? probably create dummy episode 179 if len(all_epis) == 0: 180 episode = emr.add_episode(episode_name = _('Documents'), is_open = False) 181 else: 182 from Gnumed.wxpython.gmEMRStructWidgets import cEpisodeListSelectorDlg 183 dlg = cEpisodeListSelectorDlg(parent, -1, episodes = all_epis) 184 dlg.SetTitle(_('Select the episode under which to file the document ...')) 185 btn_pressed = dlg.ShowModal() 186 episode = dlg.get_selected_item_data(only_one = True) 187 dlg.DestroyLater() 188 189 if (btn_pressed == wx.ID_CANCEL) or (episode is None): 190 if unlock_patient: 191 pat.locked = False 192 return None 193 194 wx.BeginBusyCursor() 195 196 if pk_document_type is None: 197 pk_document_type = gmDocuments.create_document_type(document_type = document_type)['pk_doc_type'] 198 199 docs_folder = pat.get_document_folder() 200 doc = docs_folder.add_document ( 201 document_type = pk_document_type, 202 encounter = emr.active_encounter['pk_encounter'], 203 episode = episode['pk_episode'] 204 ) 205 if doc is None: 206 wx.EndBusyCursor() 207 gmGuiHelpers.gm_show_error ( 208 aMessage = _('Cannot create new document.'), 209 aTitle = _('saving document') 210 ) 211 return False 212 213 if reference is not None: 214 doc['ext_ref'] = reference 215 if pk_org_unit is not None: 216 doc['pk_org_unit'] = pk_org_unit 217 if date_generated is not None: 218 doc['clin_when'] = date_generated 219 if comment is not None: 220 if comment != '': 221 doc['comment'] = comment 222 doc.save() 223 224 success, msg, filename = doc.add_parts_from_files(files = filenames, reviewer = reviewer) 225 if not success: 226 wx.EndBusyCursor() 227 gmGuiHelpers.gm_show_error ( 228 aMessage = msg, 229 aTitle = _('saving document') 230 ) 231 return False 232 233 if review_as_normal: 234 doc.set_reviewed(technically_abnormal = False, clinically_relevant = False) 235 236 if unlock_patient: 237 pat.locked = False 238 239 gmDispatcher.send(signal = 'statustext', msg = _('Imported new document from %s.') % filenames, beep = True) 240 241 # inform user 242 cfg = gmCfg.cCfgSQL() 243 show_id = bool ( 244 cfg.get2 ( 245 option = 'horstspace.scan_index.show_doc_id', 246 workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace, 247 bias = 'user' 248 ) 249 ) 250 251 wx.EndBusyCursor() 252 253 if not show_id: 254 gmDispatcher.send(signal = 'statustext', msg = _('Successfully saved new document.')) 255 else: 256 if reference is None: 257 msg = _('Successfully saved the new document.') 258 else: 259 msg = _('The reference ID for the new document is:\n' 260 '\n' 261 ' <%s>\n' 262 '\n' 263 'You probably want to write it down on the\n' 264 'original documents.\n' 265 '\n' 266 "If you don't care about the ID you can switch\n" 267 'off this message in the GNUmed configuration.\n' 268 ) % reference 269 gmGuiHelpers.gm_show_info ( 270 aMessage = msg, 271 aTitle = _('Saving document') 272 ) 273 274 # remove non-temp files 275 tmp_dir = gmTools.gmPaths().tmp_dir 276 files2remove = [ f for f in filenames if not f.startswith(tmp_dir) ] 277 if len(files2remove) > 0: 278 do_delete = gmGuiHelpers.gm_show_question ( 279 _( 'Successfully imported files as document.\n' 280 '\n' 281 'Do you want to delete imported files from the filesystem ?\n' 282 '\n' 283 ' %s' 284 ) % '\n '.join(files2remove), 285 _('Removing files') 286 ) 287 if do_delete: 288 for fname in files2remove: 289 gmTools.remove_file(fname) 290 291 return doc
292 293 #---------------------- 294 gmDispatcher.connect(signal = 'import_document_from_file', receiver = _save_file_as_new_document) 295 gmDispatcher.connect(signal = 'import_document_from_files', receiver = _save_files_as_new_document) 296 297 #============================================================
298 -class cDocumentPhraseWheel(gmPhraseWheel.cPhraseWheel):
299
300 - def __init__(self, *args, **kwargs):
301 302 gmPhraseWheel.cPhraseWheel.__init__(self, *args, **kwargs) 303 304 ctxt = {'ctxt_pat': { 305 'where_part': '(pk_patient = %(pat)s) AND', 306 'placeholder': 'pat' 307 }} 308 309 mp = gmMatchProvider.cMatchProvider_SQL2 ( 310 queries = [""" 311 SELECT DISTINCT ON (list_label) 312 pk_doc AS data, 313 l10n_type || ' (' || to_char(clin_when, 'YYYY Mon DD') || ')' || coalesce(': ' || unit || '@' || organization, '') || ' - ' || episode || coalesce(' (' || health_issue || ')', '') AS list_label, 314 l10n_type || ' (' || to_char(clin_when, 'YYYY Mon DD') || ')' || coalesce(': ' || organization, '') || ' - ' || coalesce(' (' || health_issue || ')', episode) AS field_label 315 FROM blobs.v_doc_med 316 WHERE 317 %(ctxt_pat)s 318 ( 319 l10n_type %(fragment_condition)s 320 OR 321 unit %(fragment_condition)s 322 OR 323 organization %(fragment_condition)s 324 OR 325 episode %(fragment_condition)s 326 OR 327 health_issue %(fragment_condition)s 328 ) 329 ORDER BY list_label 330 LIMIT 25"""], 331 context = ctxt 332 ) 333 mp.setThresholds(1, 3, 5) 334 #mp.unset_context('pat') 335 pat = gmPerson.gmCurrentPatient() 336 mp.set_context('pat', pat.ID) 337 338 self.matcher = mp 339 self.picklist_delay = 50 340 self.selection_only = True 341 342 self.SetToolTip(_('Select a document.'))
343 344 #--------------------------------------------------------
345 - def _data2instance(self):
346 if len(self._data) == 0: 347 return None 348 return gmDocuments.cDocument(aPK_obj = self.GetData())
349 350 #--------------------------------------------------------
351 - def _get_data_tooltip(self):
352 if len(self._data) == 0: 353 return '' 354 return gmDocuments.cDocument(aPK_obj = self.GetData()).format(single_line = False)
355 356 #============================================================
357 -class cDocumentCommentPhraseWheel(gmPhraseWheel.cPhraseWheel):
358 """Let user select a document comment from all existing comments."""
359 - def __init__(self, *args, **kwargs):
360 361 gmPhraseWheel.cPhraseWheel.__init__(self, *args, **kwargs) 362 363 context = { 364 'ctxt_doc_type': { 365 'where_part': 'and fk_type = %(pk_doc_type)s', 366 'placeholder': 'pk_doc_type' 367 } 368 } 369 370 mp = gmMatchProvider.cMatchProvider_SQL2 ( 371 queries = [""" 372 SELECT 373 data, 374 field_label, 375 list_label 376 FROM ( 377 SELECT DISTINCT ON (field_label) * 378 FROM ( 379 -- constrained by doc type 380 SELECT 381 comment AS data, 382 comment AS field_label, 383 comment AS list_label, 384 1 AS rank 385 FROM blobs.doc_med 386 WHERE 387 comment %(fragment_condition)s 388 %(ctxt_doc_type)s 389 390 UNION ALL 391 392 SELECT 393 comment AS data, 394 comment AS field_label, 395 comment AS list_label, 396 2 AS rank 397 FROM blobs.doc_med 398 WHERE 399 comment %(fragment_condition)s 400 ) AS q_union 401 ) AS q_distinct 402 ORDER BY rank, list_label 403 LIMIT 25"""], 404 context = context 405 ) 406 mp.setThresholds(3, 5, 7) 407 mp.unset_context('pk_doc_type') 408 409 self.matcher = mp 410 self.picklist_delay = 50 411 412 self.SetToolTip(_('Enter a comment on the document.'))
413 414 #============================================================ 415 # document type widgets 416 #============================================================
417 -def manage_document_types(parent=None):
418 419 if parent is None: 420 parent = wx.GetApp().GetTopWindow() 421 422 dlg = cEditDocumentTypesDlg(parent = parent) 423 dlg.ShowModal()
424 425 #============================================================ 426 from Gnumed.wxGladeWidgets import wxgEditDocumentTypesDlg 427
428 -class cEditDocumentTypesDlg(wxgEditDocumentTypesDlg.wxgEditDocumentTypesDlg):
429 """A dialog showing a cEditDocumentTypesPnl.""" 430
431 - def __init__(self, *args, **kwargs):
433 434 #============================================================ 435 from Gnumed.wxGladeWidgets import wxgEditDocumentTypesPnl 436
437 -class cEditDocumentTypesPnl(wxgEditDocumentTypesPnl.wxgEditDocumentTypesPnl):
438 """A panel grouping together fields to edit the list of document types.""" 439
440 - def __init__(self, *args, **kwargs):
441 wxgEditDocumentTypesPnl.wxgEditDocumentTypesPnl.__init__(self, *args, **kwargs) 442 self.__init_ui() 443 self.__register_interests() 444 self.repopulate_ui()
445 #--------------------------------------------------------
446 - def __init_ui(self):
447 self._LCTRL_doc_type.set_columns([_('Type'), _('Translation'), _('User defined'), _('In use')]) 448 self._LCTRL_doc_type.set_column_widths()
449 #--------------------------------------------------------
450 - def __register_interests(self):
451 gmDispatcher.connect(signal = 'blobs.doc_type_mod_db', receiver = self._on_doc_type_mod_db)
452 #--------------------------------------------------------
453 - def _on_doc_type_mod_db(self):
454 self.repopulate_ui()
455 #--------------------------------------------------------
456 - def repopulate_ui(self):
457 458 self._LCTRL_doc_type.DeleteAllItems() 459 460 doc_types = gmDocuments.get_document_types() 461 pos = len(doc_types) + 1 462 463 for doc_type in doc_types: 464 row_num = self._LCTRL_doc_type.InsertItem(pos, label = doc_type['type']) 465 self._LCTRL_doc_type.SetItem(index = row_num, column = 1, label = doc_type['l10n_type']) 466 if doc_type['is_user_defined']: 467 self._LCTRL_doc_type.SetItem(index = row_num, column = 2, label = ' X ') 468 if doc_type['is_in_use']: 469 self._LCTRL_doc_type.SetItem(index = row_num, column = 3, label = ' X ') 470 471 if len(doc_types) > 0: 472 self._LCTRL_doc_type.set_data(data = doc_types) 473 self._LCTRL_doc_type.SetColumnWidth(0, wx.LIST_AUTOSIZE) 474 self._LCTRL_doc_type.SetColumnWidth(1, wx.LIST_AUTOSIZE) 475 self._LCTRL_doc_type.SetColumnWidth(2, wx.LIST_AUTOSIZE_USEHEADER) 476 self._LCTRL_doc_type.SetColumnWidth(3, wx.LIST_AUTOSIZE_USEHEADER) 477 478 self._TCTRL_type.SetValue('') 479 self._TCTRL_l10n_type.SetValue('') 480 481 self._BTN_set_translation.Enable(False) 482 self._BTN_delete.Enable(False) 483 self._BTN_add.Enable(False) 484 self._BTN_reassign.Enable(False) 485 486 self._LCTRL_doc_type.SetFocus()
487 #-------------------------------------------------------- 488 # event handlers 489 #--------------------------------------------------------
490 - def _on_list_item_selected(self, evt):
491 doc_type = self._LCTRL_doc_type.get_selected_item_data() 492 493 self._TCTRL_type.SetValue(doc_type['type']) 494 self._TCTRL_l10n_type.SetValue(doc_type['l10n_type']) 495 496 self._BTN_set_translation.Enable(True) 497 self._BTN_delete.Enable(not bool(doc_type['is_in_use'])) 498 self._BTN_add.Enable(False) 499 self._BTN_reassign.Enable(True) 500 501 return
502 #--------------------------------------------------------
503 - def _on_type_modified(self, event):
504 self._BTN_set_translation.Enable(False) 505 self._BTN_delete.Enable(False) 506 self._BTN_reassign.Enable(False) 507 508 self._BTN_add.Enable(True) 509 # self._LCTRL_doc_type.deselect_selected_item() 510 return
511 #--------------------------------------------------------
512 - def _on_set_translation_button_pressed(self, event):
513 doc_type = self._LCTRL_doc_type.get_selected_item_data() 514 if doc_type.set_translation(translation = self._TCTRL_l10n_type.GetValue().strip()): 515 self.repopulate_ui() 516 517 return
518 #--------------------------------------------------------
519 - def _on_delete_button_pressed(self, event):
520 doc_type = self._LCTRL_doc_type.get_selected_item_data() 521 if doc_type['is_in_use']: 522 gmGuiHelpers.gm_show_info ( 523 _( 524 'Cannot delete document type\n' 525 ' [%s]\n' 526 'because it is currently in use.' 527 ) % doc_type['l10n_type'], 528 _('deleting document type') 529 ) 530 return 531 532 gmDocuments.delete_document_type(document_type = doc_type) 533 534 return
535 #--------------------------------------------------------
536 - def _on_add_button_pressed(self, event):
537 desc = self._TCTRL_type.GetValue().strip() 538 if desc != '': 539 doc_type = gmDocuments.create_document_type(document_type = desc) # does not create dupes 540 l10n_desc = self._TCTRL_l10n_type.GetValue().strip() 541 if (l10n_desc != '') and (l10n_desc != doc_type['l10n_type']): 542 doc_type.set_translation(translation = l10n_desc) 543 544 return
545 #--------------------------------------------------------
546 - def _on_reassign_button_pressed(self, event):
547 548 orig_type = self._LCTRL_doc_type.get_selected_item_data() 549 doc_types = gmDocuments.get_document_types() 550 551 new_type = gmListWidgets.get_choices_from_list ( 552 parent = self, 553 msg = _( 554 'From the list below select the document type you want\n' 555 'all documents currently classified as:\n\n' 556 ' "%s"\n\n' 557 'to be changed to.\n\n' 558 'Be aware that this change will be applied to ALL such documents. If there\n' 559 'are many documents to change it can take quite a while.\n\n' 560 'Make sure this is what you want to happen !\n' 561 ) % orig_type['l10n_type'], 562 caption = _('Reassigning document type'), 563 choices = [ [gmTools.bool2subst(dt['is_user_defined'], 'X', ''), dt['type'], dt['l10n_type']] for dt in doc_types ], 564 columns = [_('User defined'), _('Type'), _('Translation')], 565 data = doc_types, 566 single_selection = True 567 ) 568 569 if new_type is None: 570 return 571 572 wx.BeginBusyCursor() 573 gmDocuments.reclassify_documents_by_type(original_type = orig_type, target_type = new_type) 574 wx.EndBusyCursor() 575 576 return
577 578 #============================================================
579 -class cDocumentTypeSelectionPhraseWheel(gmPhraseWheel.cPhraseWheel):
580 """Let user select a document type."""
581 - def __init__(self, *args, **kwargs):
582 583 gmPhraseWheel.cPhraseWheel.__init__(self, *args, **kwargs) 584 585 mp = gmMatchProvider.cMatchProvider_SQL2 ( 586 queries = [ 587 """SELECT 588 data, 589 field_label, 590 list_label 591 FROM (( 592 SELECT 593 pk_doc_type AS data, 594 l10n_type AS field_label, 595 l10n_type AS list_label, 596 1 AS rank 597 FROM blobs.v_doc_type 598 WHERE 599 is_user_defined IS True 600 AND 601 l10n_type %(fragment_condition)s 602 ) UNION ( 603 SELECT 604 pk_doc_type AS data, 605 l10n_type AS field_label, 606 l10n_type AS list_label, 607 2 AS rank 608 FROM blobs.v_doc_type 609 WHERE 610 is_user_defined IS False 611 AND 612 l10n_type %(fragment_condition)s 613 )) AS q1 614 ORDER BY q1.rank, q1.list_label"""] 615 ) 616 mp.setThresholds(2, 4, 6) 617 618 self.matcher = mp 619 self.picklist_delay = 50 620 621 self.SetToolTip(_('Select the document type.'))
622 #--------------------------------------------------------
623 - def _create_data(self):
624 625 doc_type = self.GetValue().strip() 626 if doc_type == '': 627 gmDispatcher.send(signal = 'statustext', msg = _('Cannot create document type without name.'), beep = True) 628 _log.debug('cannot create document type without name') 629 return 630 631 pk = gmDocuments.create_document_type(doc_type)['pk_doc_type'] 632 if pk is None: 633 self.data = {} 634 else: 635 self.SetText ( 636 value = doc_type, 637 data = pk 638 )
639 640 #============================================================ 641 # document review widgets 642 #============================================================
643 -def review_document_part(parent=None, part=None):
644 if parent is None: 645 parent = wx.GetApp().GetTopWindow() 646 dlg = cReviewDocPartDlg ( 647 parent = parent, 648 id = -1, 649 part = part 650 ) 651 dlg.ShowModal() 652 dlg.DestroyLater()
653 654 #------------------------------------------------------------
655 -def review_document(parent=None, document=None):
656 return review_document_part(parent = parent, part = document)
657 658 #------------------------------------------------------------ 659 from Gnumed.wxGladeWidgets import wxgReviewDocPartDlg 660
661 -class cReviewDocPartDlg(wxgReviewDocPartDlg.wxgReviewDocPartDlg):
662 - def __init__(self, *args, **kwds):
663 """Support parts and docs now. 664 """ 665 part = kwds['part'] 666 del kwds['part'] 667 wxgReviewDocPartDlg.wxgReviewDocPartDlg.__init__(self, *args, **kwds) 668 669 if isinstance(part, gmDocuments.cDocumentPart): 670 self.__part = part 671 self.__doc = self.__part.containing_document 672 self.__reviewing_doc = False 673 elif isinstance(part, gmDocuments.cDocument): 674 self.__doc = part 675 if len(self.__doc.parts) == 0: 676 self.__part = None 677 else: 678 self.__part = self.__doc.parts[0] 679 self.__reviewing_doc = True 680 else: 681 raise ValueError('<part> must be gmDocuments.cDocument or gmDocuments.cDocumentPart instance, got <%s>' % type(part)) 682 683 self.__init_ui_data()
684 685 #-------------------------------------------------------- 686 # internal API 687 #--------------------------------------------------------
688 - def __init_ui_data(self):
689 # FIXME: fix this 690 # associated episode (add " " to avoid popping up pick list) 691 self._PhWheel_episode.SetText('%s ' % self.__doc['episode'], self.__doc['pk_episode']) 692 self._PhWheel_doc_type.SetText(value = self.__doc['l10n_type'], data = self.__doc['pk_type']) 693 self._PhWheel_doc_type.add_callback_on_set_focus(self._on_doc_type_gets_focus) 694 self._PhWheel_doc_type.add_callback_on_lose_focus(self._on_doc_type_loses_focus) 695 696 if self.__reviewing_doc: 697 self._PRW_doc_comment.SetText(gmTools.coalesce(self.__doc['comment'], '')) 698 self._PRW_doc_comment.set_context(context = 'pk_doc_type', val = self.__doc['pk_type']) 699 else: 700 self._PRW_doc_comment.SetText(gmTools.coalesce(self.__part['obj_comment'], '')) 701 702 if self.__doc['pk_org_unit'] is not None: 703 self._PRW_org.SetText(value = '%s @ %s' % (self.__doc['unit'], self.__doc['organization']), data = self.__doc['pk_org_unit']) 704 705 if self.__doc['unit_is_receiver']: 706 self._RBTN_org_is_receiver.Value = True 707 else: 708 self._RBTN_org_is_source.Value = True 709 710 if self.__reviewing_doc: 711 self._PRW_org.Enable() 712 else: 713 self._PRW_org.Disable() 714 715 if self.__doc['pk_hospital_stay'] is not None: 716 self._PRW_hospital_stay.SetText(data = self.__doc['pk_hospital_stay']) 717 718 fts = gmDateTime.cFuzzyTimestamp(timestamp = self.__doc['clin_when']) 719 self._PhWheel_doc_date.SetText(fts.strftime('%Y-%m-%d'), fts) 720 self._TCTRL_reference.SetValue(gmTools.coalesce(self.__doc['ext_ref'], '')) 721 if self.__reviewing_doc: 722 self._TCTRL_filename.Enable(False) 723 self._SPINCTRL_seq_idx.Enable(False) 724 else: 725 self._TCTRL_filename.SetValue(gmTools.coalesce(self.__part['filename'], '')) 726 self._SPINCTRL_seq_idx.SetValue(gmTools.coalesce(self.__part['seq_idx'], 0)) 727 728 self._LCTRL_existing_reviews.InsertColumn(0, _('who')) 729 self._LCTRL_existing_reviews.InsertColumn(1, _('when')) 730 self._LCTRL_existing_reviews.InsertColumn(2, _('+/-')) 731 self._LCTRL_existing_reviews.InsertColumn(3, _('!')) 732 self._LCTRL_existing_reviews.InsertColumn(4, _('comment')) 733 734 self.__reload_existing_reviews() 735 736 if self._LCTRL_existing_reviews.GetItemCount() > 0: 737 self._LCTRL_existing_reviews.SetColumnWidth(0, wx.LIST_AUTOSIZE) 738 self._LCTRL_existing_reviews.SetColumnWidth(1, wx.LIST_AUTOSIZE) 739 self._LCTRL_existing_reviews.SetColumnWidth(2, wx.LIST_AUTOSIZE_USEHEADER) 740 self._LCTRL_existing_reviews.SetColumnWidth(3, wx.LIST_AUTOSIZE_USEHEADER) 741 self._LCTRL_existing_reviews.SetColumnWidth(4, wx.LIST_AUTOSIZE) 742 743 if self.__part is None: 744 self._ChBOX_review.SetValue(False) 745 self._ChBOX_review.Enable(False) 746 self._ChBOX_abnormal.Enable(False) 747 self._ChBOX_relevant.Enable(False) 748 self._ChBOX_sign_all_pages.Enable(False) 749 else: 750 me = gmStaff.gmCurrentProvider() 751 if self.__part['pk_intended_reviewer'] == me['pk_staff']: 752 msg = _('(you are the primary reviewer)') 753 else: 754 other = gmStaff.cStaff(aPK_obj = self.__part['pk_intended_reviewer']) 755 msg = _('(someone else is the intended reviewer: %s)') % other['short_alias'] 756 self._TCTRL_responsible.SetValue(msg) 757 # init my review if any 758 if self.__part['reviewed_by_you']: 759 revs = self.__part.get_reviews() 760 for rev in revs: 761 if rev['is_your_review']: 762 self._ChBOX_abnormal.SetValue(bool(rev[2])) 763 self._ChBOX_relevant.SetValue(bool(rev[3])) 764 break 765 766 self._ChBOX_sign_all_pages.SetValue(self.__reviewing_doc) 767 768 return True
769 770 #--------------------------------------------------------
771 - def __reload_existing_reviews(self):
772 self._LCTRL_existing_reviews.DeleteAllItems() 773 if self.__part is None: 774 return True 775 revs = self.__part.get_reviews() # FIXME: this is ugly as sin, it should be dicts, not lists 776 if len(revs) == 0: 777 return True 778 # find special reviews 779 review_by_responsible_doc = None 780 reviews_by_others = [] 781 for rev in revs: 782 if rev['is_review_by_responsible_reviewer'] and not rev['is_your_review']: 783 review_by_responsible_doc = rev 784 if not (rev['is_review_by_responsible_reviewer'] or rev['is_your_review']): 785 reviews_by_others.append(rev) 786 # display them 787 if review_by_responsible_doc is not None: 788 row_num = self._LCTRL_existing_reviews.InsertItem(sys.maxsize, label=review_by_responsible_doc[0]) 789 self._LCTRL_existing_reviews.SetItemTextColour(row_num, wx.BLUE) 790 self._LCTRL_existing_reviews.SetItem(index = row_num, column=0, label=review_by_responsible_doc[0]) 791 self._LCTRL_existing_reviews.SetItem(index = row_num, column=1, label=review_by_responsible_doc[1].strftime('%x %H:%M')) 792 if review_by_responsible_doc['is_technically_abnormal']: 793 self._LCTRL_existing_reviews.SetItem(index = row_num, column=2, label='X') 794 if review_by_responsible_doc['clinically_relevant']: 795 self._LCTRL_existing_reviews.SetItem(index = row_num, column=3, label='X') 796 self._LCTRL_existing_reviews.SetItem(index = row_num, column=4, label=review_by_responsible_doc[6]) 797 row_num += 1 798 for rev in reviews_by_others: 799 row_num = self._LCTRL_existing_reviews.InsertItem(sys.maxsize, label=rev[0]) 800 self._LCTRL_existing_reviews.SetItem(index = row_num, column=0, label=rev[0]) 801 self._LCTRL_existing_reviews.SetItem(index = row_num, column=1, label=rev[1].strftime('%x %H:%M')) 802 if rev['is_technically_abnormal']: 803 self._LCTRL_existing_reviews.SetItem(index = row_num, column=2, label='X') 804 if rev['clinically_relevant']: 805 self._LCTRL_existing_reviews.SetItem(index = row_num, column=3, label='X') 806 self._LCTRL_existing_reviews.SetItem(index = row_num, column=4, label=rev[6]) 807 return True
808 809 #-------------------------------------------------------- 810 # event handlers 811 #--------------------------------------------------------
812 - def _on_save_button_pressed(self, evt):
813 """Save the metadata to the backend.""" 814 815 evt.Skip() 816 817 # 1) handle associated episode 818 pk_episode = self._PhWheel_episode.GetData(can_create=True, is_open=True) 819 if pk_episode is None: 820 gmGuiHelpers.gm_show_error ( 821 _('Cannot create episode\n [%s]'), 822 _('Editing document properties') 823 ) 824 return False 825 826 doc_type = self._PhWheel_doc_type.GetData(can_create = True) 827 if doc_type is None: 828 gmDispatcher.send(signal='statustext', msg=_('Cannot change document type to [%s].') % self._PhWheel_doc_type.GetValue().strip()) 829 return False 830 831 # since the phrasewheel operates on the active 832 # patient all episodes really should belong 833 # to it so we don't check patient change 834 self.__doc['pk_episode'] = pk_episode 835 self.__doc['pk_type'] = doc_type 836 if self.__reviewing_doc: 837 self.__doc['comment'] = self._PRW_doc_comment.GetValue().strip() 838 # FIXME: a rather crude way of error checking: 839 if self._PhWheel_doc_date.GetData() is not None: 840 self.__doc['clin_when'] = self._PhWheel_doc_date.GetData().get_pydt() 841 self.__doc['ext_ref'] = self._TCTRL_reference.GetValue().strip() 842 self.__doc['pk_org_unit'] = self._PRW_org.GetData() 843 if self._RBTN_org_is_receiver.Value is True: 844 self.__doc['unit_is_receiver'] = True 845 else: 846 self.__doc['unit_is_receiver'] = False 847 self.__doc['pk_hospital_stay'] = self._PRW_hospital_stay.GetData() 848 849 success, data = self.__doc.save() 850 if not success: 851 gmGuiHelpers.gm_show_error ( 852 _('Cannot link the document to episode\n\n [%s]') % epi_name, 853 _('Editing document properties') 854 ) 855 return False 856 857 # 2) handle review 858 if self._ChBOX_review.GetValue(): 859 provider = gmStaff.gmCurrentProvider() 860 abnormal = self._ChBOX_abnormal.GetValue() 861 relevant = self._ChBOX_relevant.GetValue() 862 msg = None 863 if self.__reviewing_doc: # - on all pages 864 if not self.__doc.set_reviewed(technically_abnormal = abnormal, clinically_relevant = relevant): 865 msg = _('Error setting "reviewed" status of this document.') 866 if self._ChBOX_responsible.GetValue(): 867 if not self.__doc.set_primary_reviewer(reviewer = provider['pk_staff']): 868 msg = _('Error setting responsible clinician for this document.') 869 else: # - just on this page 870 if not self.__part.set_reviewed(technically_abnormal = abnormal, clinically_relevant = relevant): 871 msg = _('Error setting "reviewed" status of this part.') 872 if self._ChBOX_responsible.GetValue(): 873 self.__part['pk_intended_reviewer'] = provider['pk_staff'] 874 if msg is not None: 875 gmGuiHelpers.gm_show_error(msg, _('Editing document properties')) 876 return False 877 878 # 3) handle "page" specific parts 879 if not self.__reviewing_doc: 880 self.__part['filename'] = gmTools.none_if(self._TCTRL_filename.GetValue().strip(), '') 881 new_idx = gmTools.none_if(self._SPINCTRL_seq_idx.GetValue(), 0) 882 if self.__part['seq_idx'] != new_idx: 883 if new_idx in self.__doc['seq_idx_list']: 884 msg = _( 885 'Cannot set page number to [%s] because\n' 886 'another page with this number exists.\n' 887 '\n' 888 'Page numbers in use:\n' 889 '\n' 890 ' %s' 891 ) % ( 892 new_idx, 893 self.__doc['seq_idx_list'] 894 ) 895 gmGuiHelpers.gm_show_error(msg, _('Editing document part properties')) 896 else: 897 self.__part['seq_idx'] = new_idx 898 self.__part['obj_comment'] = self._PRW_doc_comment.GetValue().strip() 899 success, data = self.__part.save_payload() 900 if not success: 901 gmGuiHelpers.gm_show_error ( 902 _('Error saving part properties.'), 903 _('Editing document part properties') 904 ) 905 return False 906 907 return True
908 909 #--------------------------------------------------------
910 - def _on_reviewed_box_checked(self, evt):
911 state = self._ChBOX_review.GetValue() 912 self._ChBOX_abnormal.Enable(enable = state) 913 self._ChBOX_relevant.Enable(enable = state) 914 self._ChBOX_responsible.Enable(enable = state)
915 916 #--------------------------------------------------------
917 - def _on_doc_type_gets_focus(self):
918 """Per Jim: Changing the doc type happens a lot more often 919 then correcting spelling, hence select-all on getting focus. 920 """ 921 self._PhWheel_doc_type.SetSelection(-1, -1)
922 923 #--------------------------------------------------------
924 - def _on_doc_type_loses_focus(self):
925 pk_doc_type = self._PhWheel_doc_type.GetData() 926 if pk_doc_type is None: 927 self._PRW_doc_comment.unset_context(context = 'pk_doc_type') 928 else: 929 self._PRW_doc_comment.set_context(context = 'pk_doc_type', val = pk_doc_type) 930 return True
931 932 #============================================================
933 -def acquire_images_from_capture_device(device=None, calling_window=None):
934 935 _log.debug('acquiring images from [%s]', device) 936 937 # do not import globally since we might want to use 938 # this module without requiring any scanner to be available 939 from Gnumed.pycommon import gmScanBackend 940 try: 941 fnames = gmScanBackend.acquire_pages_into_files ( 942 device = device, 943 delay = 5, 944 calling_window = calling_window 945 ) 946 except OSError: 947 _log.exception('problem acquiring image from source') 948 gmGuiHelpers.gm_show_error ( 949 aMessage = _( 950 'No images could be acquired from the source.\n\n' 951 'This may mean the scanner driver is not properly installed.\n\n' 952 'On Windows you must install the TWAIN Python module\n' 953 'while on Linux and MacOSX it is recommended to install\n' 954 'the XSane package.' 955 ), 956 aTitle = _('Acquiring images') 957 ) 958 return None 959 960 _log.debug('acquired %s images', len(fnames)) 961 962 return fnames
963 964 #------------------------------------------------------------ 965 from Gnumed.wxGladeWidgets import wxgScanIdxPnl 966
967 -class cScanIdxDocsPnl(wxgScanIdxPnl.wxgScanIdxPnl, gmPlugin.cPatientChange_PluginMixin):
968
969 - def __init__(self, *args, **kwds):
970 wxgScanIdxPnl.wxgScanIdxPnl.__init__(self, *args, **kwds) 971 gmPlugin.cPatientChange_PluginMixin.__init__(self) 972 973 self._PhWheel_reviewer.matcher = gmPerson.cMatchProvider_Provider() 974 975 self.__init_ui_data() 976 self._PhWheel_doc_type.add_callback_on_lose_focus(self._on_doc_type_loses_focus) 977 978 # make me and listctrl file drop targets 979 dt = gmGuiHelpers.cFileDropTarget(target = self) 980 self.SetDropTarget(dt) 981 dt = gmGuiHelpers.cFileDropTarget(on_drop_callback = self._drop_target_consume_filenames) 982 self._LCTRL_doc_pages.SetDropTarget(dt) 983 984 # do not import globally since we might want to use 985 # this module without requiring any scanner to be available 986 from Gnumed.pycommon import gmScanBackend 987 self.scan_module = gmScanBackend
988 989 #-------------------------------------------------------- 990 # file drop target API 991 #--------------------------------------------------------
992 - def _drop_target_consume_filenames(self, filenames):
993 pat = gmPerson.gmCurrentPatient() 994 if not pat.connected: 995 gmDispatcher.send(signal='statustext', msg=_('Cannot accept new documents. No active patient.')) 996 return 997 998 # dive into folders dropped onto us and extract files (one level deep only) 999 real_filenames = [] 1000 for pathname in filenames: 1001 try: 1002 files = os.listdir(pathname) 1003 source = _('directory dropped on client') 1004 gmDispatcher.send(signal = 'statustext', msg = _('Extracting files from folder [%s] ...') % pathname) 1005 for filename in files: 1006 fullname = os.path.join(pathname, filename) 1007 if not os.path.isfile(fullname): 1008 continue 1009 real_filenames.append(fullname) 1010 except OSError: 1011 source = _('file dropped on client') 1012 real_filenames.append(pathname) 1013 1014 self.add_parts_from_files(real_filenames, source)
1015 1016 #--------------------------------------------------------
1017 - def repopulate_ui(self):
1018 pass
1019 1020 #-------------------------------------------------------- 1021 # patient change plugin API 1022 #--------------------------------------------------------
1023 - def _pre_patient_unselection(self, **kwds):
1024 # FIXME: persist pending data from here 1025 pass
1026 1027 #--------------------------------------------------------
1028 - def _post_patient_selection(self, **kwds):
1029 self.__init_ui_data()
1030 1031 #-------------------------------------------------------- 1032 # internal API 1033 #--------------------------------------------------------
1034 - def __init_ui_data(self):
1035 # ----------------------------- 1036 self._PhWheel_episode.SetText(value = _('other documents'), suppress_smarts = True) 1037 self._PhWheel_doc_type.SetText('') 1038 # ----------------------------- 1039 # FIXME: make this configurable: either now() or last_date() 1040 fts = gmDateTime.cFuzzyTimestamp() 1041 self._PhWheel_doc_date.SetText(fts.strftime('%Y-%m-%d'), fts) 1042 self._PRW_doc_comment.SetText('') 1043 self._PhWheel_source.SetText('', None) 1044 self._RBTN_org_is_source.SetValue(1) 1045 # FIXME: should be set to patient's primary doc 1046 self._PhWheel_reviewer.selection_only = True 1047 me = gmStaff.gmCurrentProvider() 1048 self._PhWheel_reviewer.SetText ( 1049 value = '%s (%s%s %s)' % (me['short_alias'], gmTools.coalesce(me['title'], ''), me['firstnames'], me['lastnames']), 1050 data = me['pk_staff'] 1051 ) 1052 # ----------------------------- 1053 # FIXME: set from config item 1054 self._ChBOX_reviewed.SetValue(False) 1055 self._ChBOX_abnormal.Disable() 1056 self._ChBOX_abnormal.SetValue(False) 1057 self._ChBOX_relevant.Disable() 1058 self._ChBOX_relevant.SetValue(False) 1059 # ----------------------------- 1060 self._TBOX_description.SetValue('') 1061 # ----------------------------- 1062 # the list holding our page files 1063 self._LCTRL_doc_pages.remove_items_safely() 1064 self._LCTRL_doc_pages.set_columns([_('file'), _('path')]) 1065 self._LCTRL_doc_pages.set_column_widths() 1066 1067 self._TCTRL_metadata.SetValue('') 1068 1069 self._PhWheel_doc_type.SetFocus()
1070 1071 #--------------------------------------------------------
1072 - def add_parts_from_files(self, filenames, source=''):
1073 rows = gmTools.coalesce(self._LCTRL_doc_pages.string_items, []) 1074 data = gmTools.coalesce(self._LCTRL_doc_pages.data, []) 1075 rows.extend([ [gmTools.fname_from_path(f), gmTools.fname_dir(f)] for f in filenames ]) 1076 data.extend([ [f, source] for f in filenames ]) 1077 self._LCTRL_doc_pages.string_items = rows 1078 self._LCTRL_doc_pages.data = data 1079 self._LCTRL_doc_pages.set_column_widths()
1080 1081 #--------------------------------------------------------
1082 - def __valid_for_save(self):
1083 title = _('saving document') 1084 1085 if self._LCTRL_doc_pages.ItemCount == 0: 1086 dbcfg = gmCfg.cCfgSQL() 1087 allow_empty = bool(dbcfg.get2 ( 1088 option = 'horstspace.scan_index.allow_partless_documents', 1089 workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace, 1090 bias = 'user', 1091 default = False 1092 )) 1093 if allow_empty: 1094 save_empty = gmGuiHelpers.gm_show_question ( 1095 aMessage = _('No parts to save. Really save an empty document as a reference ?'), 1096 aTitle = title 1097 ) 1098 if not save_empty: 1099 return False 1100 else: 1101 gmGuiHelpers.gm_show_error ( 1102 aMessage = _('No parts to save. Aquire some parts first.'), 1103 aTitle = title 1104 ) 1105 return False 1106 1107 doc_type_pk = self._PhWheel_doc_type.GetData(can_create = True) 1108 if doc_type_pk is None: 1109 gmGuiHelpers.gm_show_error ( 1110 aMessage = _('No document type applied. Choose a document type'), 1111 aTitle = title 1112 ) 1113 return False 1114 1115 # this should be optional, actually 1116 # if self._PRW_doc_comment.GetValue().strip() == '': 1117 # gmGuiHelpers.gm_show_error ( 1118 # aMessage = _('No document comment supplied. Add a comment for this document.'), 1119 # aTitle = title 1120 # ) 1121 # return False 1122 1123 if self._PhWheel_episode.GetValue().strip() == '': 1124 gmGuiHelpers.gm_show_error ( 1125 aMessage = _('You must select an episode to save this document under.'), 1126 aTitle = title 1127 ) 1128 return False 1129 1130 if self._PhWheel_reviewer.GetData() is None: 1131 gmGuiHelpers.gm_show_error ( 1132 aMessage = _('You need to select from the list of staff members the doctor who is intended to sign the document.'), 1133 aTitle = title 1134 ) 1135 return False 1136 1137 if self._PhWheel_doc_date.is_valid_timestamp(empty_is_valid = True) is False: 1138 gmGuiHelpers.gm_show_error ( 1139 aMessage = _('Invalid date of generation.'), 1140 aTitle = title 1141 ) 1142 return False 1143 1144 return True
1145 1146 #--------------------------------------------------------
1147 - def get_device_to_use(self, reconfigure=False):
1148 1149 if not reconfigure: 1150 dbcfg = gmCfg.cCfgSQL() 1151 device = dbcfg.get2 ( 1152 option = 'external.xsane.default_device', 1153 workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace, 1154 bias = 'workplace', 1155 default = '' 1156 ) 1157 if device.strip() == '': 1158 device = None 1159 if device is not None: 1160 return device 1161 1162 try: 1163 devices = self.scan_module.get_devices() 1164 except: 1165 _log.exception('cannot retrieve list of image sources') 1166 gmDispatcher.send(signal = 'statustext', msg = _('There is no scanner support installed on this machine.')) 1167 return None 1168 1169 if devices is None: 1170 # get_devices() not implemented for TWAIN yet 1171 # XSane has its own chooser (so does TWAIN) 1172 return None 1173 1174 if len(devices) == 0: 1175 gmDispatcher.send(signal = 'statustext', msg = _('Cannot find an active scanner.')) 1176 return None 1177 1178 # device_names = [] 1179 # for device in devices: 1180 # device_names.append('%s (%s)' % (device[2], device[0])) 1181 1182 device = gmListWidgets.get_choices_from_list ( 1183 parent = self, 1184 msg = _('Select an image capture device'), 1185 caption = _('device selection'), 1186 choices = [ '%s (%s)' % (d[2], d[0]) for d in devices ], 1187 columns = [_('Device')], 1188 data = devices, 1189 single_selection = True 1190 ) 1191 if device is None: 1192 return None 1193 1194 # FIXME: add support for actually reconfiguring 1195 return device[0]
1196 1197 #-------------------------------------------------------- 1198 # event handling API 1199 #--------------------------------------------------------
1200 - def _scan_btn_pressed(self, evt):
1201 1202 chosen_device = self.get_device_to_use() 1203 1204 # FIXME: configure whether to use XSane or sane directly 1205 # FIXME: add support for xsane_device_settings argument 1206 try: 1207 fnames = self.scan_module.acquire_pages_into_files ( 1208 device = chosen_device, 1209 delay = 5, 1210 calling_window = self 1211 ) 1212 except OSError: 1213 _log.exception('problem acquiring image from source') 1214 gmGuiHelpers.gm_show_error ( 1215 aMessage = _( 1216 'No pages could be acquired from the source.\n\n' 1217 'This may mean the scanner driver is not properly installed.\n\n' 1218 'On Windows you must install the TWAIN Python module\n' 1219 'while on Linux and MacOSX it is recommended to install\n' 1220 'the XSane package.' 1221 ), 1222 aTitle = _('acquiring page') 1223 ) 1224 return None 1225 1226 if len(fnames) == 0: # no pages scanned 1227 return True 1228 1229 self.add_parts_from_files(fnames, _('captured by imaging device')) 1230 return True
1231 1232 #--------------------------------------------------------
1233 - def _load_btn_pressed(self, evt):
1234 # patient file chooser 1235 dlg = wx.FileDialog ( 1236 parent = None, 1237 message = _('Choose a file'), 1238 defaultDir = os.path.expanduser(os.path.join('~', 'gnumed')), 1239 defaultFile = '', 1240 wildcard = "%s (*)|*|TIFFs (*.tif)|*.tif|JPEGs (*.jpg)|*.jpg|%s (*.*)|*.*" % (_('all files'), _('all files (Win)')), 1241 style = wx.FD_OPEN | wx.FD_FILE_MUST_EXIST | wx.FD_MULTIPLE 1242 ) 1243 result = dlg.ShowModal() 1244 files = dlg.GetPaths() 1245 if result == wx.ID_CANCEL: 1246 dlg.DestroyLater() 1247 return 1248 1249 self.add_parts_from_files(files, _('picked from storage media'))
1250 1251 #--------------------------------------------------------
1252 - def _clipboard_btn_pressed(self, event):
1253 event.Skip() 1254 clip = gmGuiHelpers.clipboard2file() 1255 if clip is None: 1256 return 1257 if clip is False: 1258 return 1259 self.add_parts_from_files([clip], _('pasted from clipboard'))
1260 1261 #--------------------------------------------------------
1262 - def _show_btn_pressed(self, evt):
1263 1264 # nothing to do 1265 if self._LCTRL_doc_pages.ItemCount == 0: 1266 return 1267 1268 # only one page, show that, regardless of whether selected or not 1269 if self._LCTRL_doc_pages.ItemCount == 1: 1270 page_fnames = [ self._LCTRL_doc_pages.get_item_data(0)[0] ] 1271 else: 1272 # did user select one of multiple pages ? 1273 page_fnames = [ data[0] for data in self._LCTRL_doc_pages.selected_item_data ] 1274 if len(page_fnames) == 0: 1275 gmDispatcher.send(signal = 'statustext', msg = _('No part selected for viewing.'), beep = True) 1276 return 1277 1278 for page_fname in page_fnames: 1279 (success, msg) = gmMimeLib.call_viewer_on_file(page_fname) 1280 if not success: 1281 gmGuiHelpers.gm_show_warning ( 1282 aMessage = _('Cannot display document part:\n%s') % msg, 1283 aTitle = _('displaying part') 1284 )
1285 1286 #--------------------------------------------------------
1287 - def _del_btn_pressed(self, event):
1288 1289 if len(self._LCTRL_doc_pages.selected_items) == 0: 1290 gmDispatcher.send(signal = 'statustext', msg = _('No part selected for removal.'), beep = True) 1291 return 1292 1293 sel_idx = self._LCTRL_doc_pages.GetFirstSelected() 1294 rows = self._LCTRL_doc_pages.string_items 1295 data = self._LCTRL_doc_pages.data 1296 del rows[sel_idx] 1297 del data[sel_idx] 1298 self._LCTRL_doc_pages.string_items = rows 1299 self._LCTRL_doc_pages.data = data 1300 self._LCTRL_doc_pages.set_column_widths() 1301 self._TCTRL_metadata.SetValue('')
1302 1303 #--------------------------------------------------------
1304 - def _save_btn_pressed(self, evt):
1305 1306 if not self.__valid_for_save(): 1307 return False 1308 1309 # external reference 1310 cfg = gmCfg.cCfgSQL() 1311 generate_uuid = bool ( 1312 cfg.get2 ( 1313 option = 'horstspace.scan_index.generate_doc_uuid', 1314 workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace, 1315 bias = 'user', 1316 default = False 1317 ) 1318 ) 1319 if generate_uuid: 1320 ext_ref = gmDocuments.get_ext_ref() 1321 else: 1322 ext_ref = None 1323 1324 # create document 1325 date = self._PhWheel_doc_date.GetData() 1326 if date is not None: 1327 date = date.get_pydt() 1328 new_doc = save_files_as_new_document ( 1329 parent = self, 1330 filenames = [ data[0] for data in self._LCTRL_doc_pages.data ], 1331 document_type = self._PhWheel_doc_type.GetValue().strip(), 1332 pk_document_type = self._PhWheel_doc_type.GetData(), 1333 unlock_patient = False, 1334 episode = self._PhWheel_episode.GetData(can_create = True, is_open = True, as_instance = True), 1335 review_as_normal = False, 1336 reference = ext_ref, 1337 pk_org_unit = self._PhWheel_source.GetData(), 1338 date_generated = date, 1339 comment = self._PRW_doc_comment.GetLineText(0).strip(), 1340 reviewer = self._PhWheel_reviewer.GetData() 1341 ) 1342 if new_doc is None: 1343 return False 1344 1345 if self._RBTN_org_is_receiver.Value is True: 1346 new_doc['unit_is_receiver'] = True 1347 new_doc.save() 1348 1349 # - long description 1350 description = self._TBOX_description.GetValue().strip() 1351 if description != '': 1352 if not new_doc.add_description(description): 1353 wx.EndBusyCursor() 1354 gmGuiHelpers.gm_show_error ( 1355 aMessage = _('Cannot add document description.'), 1356 aTitle = _('saving document') 1357 ) 1358 return False 1359 1360 # set reviewed status 1361 if self._ChBOX_reviewed.GetValue(): 1362 if not new_doc.set_reviewed ( 1363 technically_abnormal = self._ChBOX_abnormal.GetValue(), 1364 clinically_relevant = self._ChBOX_relevant.GetValue() 1365 ): 1366 msg = _('Error setting "reviewed" status of new document.') 1367 1368 self.__init_ui_data() 1369 1370 gmHooks.run_hook_script(hook = 'after_new_doc_created') 1371 1372 return True
1373 1374 #--------------------------------------------------------
1375 - def _startover_btn_pressed(self, evt):
1376 self.__init_ui_data()
1377 1378 #--------------------------------------------------------
1379 - def _reviewed_box_checked(self, evt):
1380 self._ChBOX_abnormal.Enable(enable = self._ChBOX_reviewed.GetValue()) 1381 self._ChBOX_relevant.Enable(enable = self._ChBOX_reviewed.GetValue())
1382 1383 #--------------------------------------------------------
1384 - def _on_doc_type_loses_focus(self):
1385 pk_doc_type = self._PhWheel_doc_type.GetData() 1386 if pk_doc_type is None: 1387 self._PRW_doc_comment.unset_context(context = 'pk_doc_type') 1388 else: 1389 self._PRW_doc_comment.set_context(context = 'pk_doc_type', val = pk_doc_type) 1390 return True
1391 1392 #--------------------------------------------------------
1393 - def _on_update_file_description(self, result):
1394 status, description = result 1395 fname, source = self._LCTRL_doc_pages.get_selected_item_data(only_one = True) 1396 txt = _( 1397 'Source: %s\n' 1398 'File: %s\n' 1399 '\n' 1400 '%s' 1401 ) % ( 1402 source, 1403 fname, 1404 description 1405 ) 1406 wx.CallAfter(self._TCTRL_metadata.SetValue, txt)
1407 1408 #--------------------------------------------------------
1409 - def _on_part_selected(self, event):
1410 event.Skip() 1411 fname, source = self._LCTRL_doc_pages.get_item_data(item_idx = event.Index) 1412 self._TCTRL_metadata.SetValue('Retrieving details from [%s] ...' % fname) 1413 gmMimeLib.describe_file(fname, callback = self._on_update_file_description)
1414 1415 #============================================================
1416 -def display_document_part(parent=None, part=None):
1417 1418 if parent is None: 1419 parent = wx.GetApp().GetTopWindow() 1420 1421 # sanity check 1422 if part['size'] == 0: 1423 _log.debug('cannot display part [%s] - 0 bytes', part['pk_obj']) 1424 gmGuiHelpers.gm_show_error ( 1425 aMessage = _('Document part does not seem to exist in database !'), 1426 aTitle = _('showing document') 1427 ) 1428 return None 1429 1430 wx.BeginBusyCursor() 1431 cfg = gmCfg.cCfgSQL() 1432 1433 # determine database export chunk size 1434 chunksize = int( 1435 cfg.get2 ( 1436 option = "horstspace.blob_export_chunk_size", 1437 workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace, 1438 bias = 'workplace', 1439 default = 2048 1440 )) 1441 1442 # shall we force blocking during view ? 1443 block_during_view = bool( cfg.get2 ( 1444 option = 'horstspace.document_viewer.block_during_view', 1445 workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace, 1446 bias = 'user', 1447 default = None 1448 )) 1449 1450 wx.EndBusyCursor() 1451 1452 # display it 1453 successful, msg = part.display_via_mime ( 1454 chunksize = chunksize, 1455 block = block_during_view 1456 ) 1457 if not successful: 1458 gmGuiHelpers.gm_show_error ( 1459 aMessage = _('Cannot display document part:\n%s') % msg, 1460 aTitle = _('showing document') 1461 ) 1462 return None 1463 1464 # handle review after display 1465 # 0: never 1466 # 1: always 1467 # 2: if no review by myself exists yet 1468 # 3: if no review at all exists yet 1469 # 4: if no review by responsible reviewer 1470 review_after_display = int(cfg.get2 ( 1471 option = 'horstspace.document_viewer.review_after_display', 1472 workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace, 1473 bias = 'user', 1474 default = 3 1475 )) 1476 if review_after_display == 1: # always review 1477 review_document_part(parent = parent, part = part) 1478 elif review_after_display == 2: # review if no review by me exists 1479 review_by_me = [ rev for rev in part.get_reviews() if rev['is_your_review'] ] 1480 if len(review_by_me) == 0: 1481 review_document_part(parent = parent, part = part) 1482 elif review_after_display == 3: 1483 if len(part.get_reviews()) == 0: 1484 review_document_part(parent = parent, part = part) 1485 elif review_after_display == 4: 1486 reviewed_by_responsible = [ rev for rev in part.get_reviews() if rev['is_review_by_responsible_reviewer'] ] 1487 if len(reviewed_by_responsible) == 0: 1488 review_document_part(parent = parent, part = part) 1489 1490 return True
1491 1492 #============================================================
1493 -def manage_documents(parent=None, msg=None, single_selection=True, pk_types=None, pk_episodes=None):
1494 1495 pat = gmPerson.gmCurrentPatient() 1496 1497 if parent is None: 1498 parent = wx.GetApp().GetTopWindow() 1499 1500 #-------------------------------------------------------- 1501 def edit(document=None): 1502 return
1503 #return edit_substance(parent = parent, substance = substance, single_entry = (substance is not None)) 1504 1505 #-------------------------------------------------------- 1506 def delete(document): 1507 return 1508 # if substance.is_in_use_by_patients: 1509 # gmDispatcher.send(signal = 'statustext', msg = _('Cannot delete this substance. It is in use.'), beep = True) 1510 # return False 1511 # 1512 # return gmMedication.delete_x_substance(substance = substance['pk']) 1513 1514 #------------------------------------------------------------ 1515 def refresh(lctrl): 1516 docs = pat.document_folder.get_documents(pk_types = pk_types, pk_episodes = pk_episodes) 1517 items = [ [ 1518 gmDateTime.pydt_strftime(d['clin_when'], '%Y %b %d', accuracy = gmDateTime.acc_days), 1519 d['l10n_type'], 1520 gmTools.coalesce(d['comment'], ''), 1521 gmTools.coalesce(d['ext_ref'], ''), 1522 d['pk_doc'] 1523 ] for d in docs ] 1524 lctrl.set_string_items(items) 1525 lctrl.set_data(docs) 1526 1527 #-------------------------------------------------------- 1528 def show_doc(doc): 1529 if doc is None: 1530 return 1531 for fname in doc.save_parts_to_files(): 1532 gmMimeLib.call_viewer_on_file(aFile = fname, block = False) 1533 1534 #------------------------------------------------------------ 1535 return gmListWidgets.get_choices_from_list ( 1536 parent = parent, 1537 caption = _('Patient document list'), 1538 columns = [_('Generated'), _('Type'), _('Comment'), _('Ref #'), '#'], 1539 single_selection = single_selection, 1540 #new_callback = edit, 1541 #edit_callback = edit, 1542 #delete_callback = delete, 1543 refresh_callback = refresh, 1544 left_extra_button = (_('Show'), _('Show all parts of this document in external viewer.'), show_doc) 1545 ) 1546 1547 #============================================================ 1548 from Gnumed.wxGladeWidgets import wxgSelectablySortedDocTreePnl 1549
1550 -class cSelectablySortedDocTreePnl(wxgSelectablySortedDocTreePnl.wxgSelectablySortedDocTreePnl):
1551 """A panel with a document tree which can be sorted. 1552 1553 On the right there's a list ctrl showing 1554 details of the selected node. 1555 """
1556 - def __init__(self, parent, id, *args, **kwds):
1557 wxgSelectablySortedDocTreePnl.wxgSelectablySortedDocTreePnl.__init__(self, parent, id, *args, **kwds) 1558 1559 self._LCTRL_details.set_columns(['', '']) 1560 1561 self.__metainfo4parts = {} 1562 self.__pk_curr_pat = None 1563 self.__pk_curr_doc_part = None 1564 1565 self._doc_tree.show_details_callback = self._update_details
1566 1567 #-------------------------------------------------------- 1568 # inherited event handlers 1569 #--------------------------------------------------------
1570 - def _on_sort_by_age_selected(self, evt):
1571 self._doc_tree.sort_mode = 'age' 1572 self._doc_tree.SetFocus() 1573 self._rbtn_sort_by_age.SetValue(True)
1574 1575 #--------------------------------------------------------
1576 - def _on_sort_by_review_selected(self, evt):
1577 self._doc_tree.sort_mode = 'review' 1578 self._doc_tree.SetFocus() 1579 self._rbtn_sort_by_review.SetValue(True)
1580 1581 #--------------------------------------------------------
1582 - def _on_sort_by_episode_selected(self, evt):
1583 self._doc_tree.sort_mode = 'episode' 1584 self._doc_tree.SetFocus() 1585 self._rbtn_sort_by_episode.SetValue(True)
1586 1587 #--------------------------------------------------------
1588 - def _on_sort_by_issue_selected(self, event):
1589 self._doc_tree.sort_mode = 'issue' 1590 self._doc_tree.SetFocus() 1591 self._rbtn_sort_by_issue.SetValue(True)
1592 1593 #--------------------------------------------------------
1594 - def _on_sort_by_type_selected(self, evt):
1595 self._doc_tree.sort_mode = 'type' 1596 self._doc_tree.SetFocus() 1597 self._rbtn_sort_by_type.SetValue(True)
1598 1599 #--------------------------------------------------------
1600 - def _on_sort_by_org_selected(self, evt):
1601 self._doc_tree.sort_mode = 'org' 1602 self._doc_tree.SetFocus() 1603 self._rbtn_sort_by_org.SetValue(True)
1604 1605 #-------------------------------------------------------- 1606 #--------------------------------------------------------
1607 - def _update_details(self, issue=None, episode=None, org_unit=None, document=None, part=None):
1608 1609 self._LCTRL_details.set_string_items([]) 1610 self._TCTRL_metainfo.Value = '' 1611 1612 if document is None: 1613 if part is not None: 1614 document = part.document 1615 1616 if issue is None: 1617 if episode is not None: 1618 issue = episode.health_issue 1619 1620 items = [] 1621 items.extend(self.__process_issue(issue)) 1622 items.extend(self.__process_episode(episode)) 1623 items.extend(self.__process_org_unit(org_unit)) 1624 items.extend(self.__process_document(document)) 1625 # keep this last so self.__pk_curr_doc_part stays current 1626 items.extend(self.__process_document_part(part)) 1627 self._LCTRL_details.set_string_items(items) 1628 self._LCTRL_details.set_column_widths() 1629 self._LCTRL_details.set_resize_column(1)
1630 1631 #-------------------------------------------------------- 1632 # internal helper logic 1633 #--------------------------------------------------------
1634 - def __check_cache_validity(self, pk_patient):
1635 if self.__pk_curr_pat == pk_patient: 1636 return 1637 self.__metainfo4parts = {}
1638 1639 #--------------------------------------------------------
1640 - def __receive_metainfo_from_worker(self, result):
1641 success, desc, pk_obj = result 1642 self.__metainfo4parts[pk_obj] = desc 1643 if not success: 1644 del self.__metainfo4parts[pk_obj] 1645 # safely cross thread boundaries 1646 wx.CallAfter(self.__update_metainfo, pk_obj)
1647 1648 #--------------------------------------------------------
1649 - def __update_metainfo(self, pk_obj, document_part=None):
1650 if self.__pk_curr_doc_part != pk_obj: 1651 # worker result arrived too late 1652 # but don't empty metainfo, might already contain cached value from new doc part node 1653 #self._TCTRL_metainfo.Value = '' 1654 return 1655 1656 try: 1657 self._TCTRL_metainfo.Value = self.__metainfo4parts[pk_obj] 1658 except KeyError: 1659 document_part.format_metainfo(callback = self.__receive_metainfo_from_worker)
1660 1661 #--------------------------------------------------------
1662 - def __process_issue(self, issue):
1663 if issue is None: 1664 return [] 1665 1666 self.__check_cache_validity(issue['pk_patient']) 1667 self.__pk_curr_doc_part = None 1668 1669 items = [] 1670 items.append([_('Health issue'), '%s%s [#%s]' % ( 1671 issue['description'], 1672 gmTools.coalesce ( 1673 value2test = issue['laterality'], 1674 return_instead = '', 1675 template4value = ' (%s)', 1676 none_equivalents = [None, '', '?'] 1677 ), 1678 issue['pk_health_issue'] 1679 )]) 1680 items.append([_('Status'), '%s, %s %s' % ( 1681 gmTools.bool2subst(issue['is_active'], _('active'), _('inactive')), 1682 gmTools.bool2subst(issue['clinically_relevant'], _('clinically relevant'), _('not clinically relevant')), 1683 issue.diagnostic_certainty_description 1684 )]) 1685 items.append([_('Confidential'), issue['is_confidential']]) 1686 items.append([_('Age noted'), issue.age_noted_human_readable()]) 1687 return items
1688 1689 #--------------------------------------------------------
1690 - def __process_episode(self, episode):
1691 if episode is None: 1692 return [] 1693 1694 self.__check_cache_validity(episode['pk_patient']) 1695 self.__pk_curr_doc_part = None 1696 1697 items = [] 1698 items.append([_('Episode'), '%s [#%s]' % ( 1699 episode['description'], 1700 episode['pk_episode'] 1701 )]) 1702 items.append([_('Status'), '%s %s' % ( 1703 gmTools.bool2subst(episode['episode_open'], _('active'), _('finished')), 1704 episode.diagnostic_certainty_description 1705 )]) 1706 items.append([_('Health issue'), gmTools.coalesce(episode['health_issue'], '')]) 1707 return items
1708 1709 #--------------------------------------------------------
1710 - def __process_org_unit(self, org_unit):
1711 if org_unit is None: 1712 return [] 1713 1714 # cannot check for cache validity: no patient reference 1715 # the doc-part-in-context, however, _will_ have changed 1716 self.__pk_curr_doc_part = None 1717 self._TCTRL_metainfo.Value = '' 1718 1719 items = [] 1720 items.append([_('Organization'), '%s (%s) [#%s]' % ( 1721 org_unit['organization'], 1722 org_unit['l10n_organization_category'], 1723 org_unit['pk_org'] 1724 )]) 1725 items.append([_('Department'), '%s%s [#%s]' % ( 1726 org_unit['unit'], 1727 gmTools.coalesce(org_unit['l10n_unit_category'], '', ' (%s)'), 1728 org_unit['pk_org_unit'] 1729 )]) 1730 adr = org_unit.address 1731 if adr is not None: 1732 lines = adr.format() 1733 items.append([lines[0], lines[1]]) 1734 for line in lines[2:]: 1735 items.append(['', line]) 1736 for comm in org_unit.comm_channels: 1737 items.append([comm['l10n_comm_type'], '%s%s' % ( 1738 comm['url'], 1739 gmTools.bool2subst(comm['is_confidential'], _(' (confidential)'), '', '') 1740 )]) 1741 return items
1742 1743 #--------------------------------------------------------
1744 - def __process_document(self, document):
1745 if document is None: 1746 return [] 1747 1748 self.__check_cache_validity(document['pk_patient']) 1749 self.__pk_curr_doc_part = None 1750 1751 items = [] 1752 items.append([_('Document'), '%s [#%s]' % (document['l10n_type'], document['pk_doc'])]) 1753 items.append([_('Generated'), gmDateTime.pydt_strftime(document['clin_when'], '%Y %b %d')]) 1754 items.append([_('Health issue'), gmTools.coalesce(document['health_issue'], '', '%%s [#%s]' % document['pk_health_issue'])]) 1755 items.append([_('Episode'), '%s (%s) [#%s]' % ( 1756 document['episode'], 1757 gmTools.bool2subst(document['episode_open'], _('open'), _('closed')), 1758 document['pk_episode'] 1759 )]) 1760 if document['pk_org_unit'] is not None: 1761 if document['unit_is_receiver']: 1762 header = _('Receiver') 1763 else: 1764 header = _('Sender') 1765 items.append([header, '%s @ %s' % (document['unit'], document['organization'])]) 1766 if document['ext_ref'] is not None: 1767 items.append([_('Reference'), document['ext_ref']]) 1768 if document['comment'] is not None: 1769 items.append([_('Comment'), ' / '.join(document['comment'].split('\n'))]) 1770 for proc in document.procedures: 1771 items.append([_('Procedure'), proc.format ( 1772 left_margin = 0, 1773 include_episode = False, 1774 include_codes = False, 1775 include_address = False, 1776 include_comm = False, 1777 include_doc = False 1778 )]) 1779 stay = document.hospital_stay 1780 if stay is not None: 1781 items.append([_('Hospital stay'), stay.format(include_episode = False)]) 1782 for bill in document.bills: 1783 items.append([_('Bill'), bill.format ( 1784 include_receiver = False, 1785 include_doc = False 1786 )]) 1787 items.append([_('Modified'), gmDateTime.pydt_strftime(document['modified_when'], '%Y %b %d')]) 1788 items.append([_('... by'), document['modified_by']]) 1789 items.append([_('# encounter'), document['pk_encounter']]) 1790 return items
1791 1792 #--------------------------------------------------------
1793 - def __process_document_part(self, document_part):
1794 if document_part is None: 1795 return [] 1796 1797 self.__check_cache_validity(document_part['pk_patient']) 1798 self.__pk_curr_doc_part = document_part['pk_obj'] 1799 self.__update_metainfo(document_part['pk_obj'], document_part) 1800 items = [] 1801 items.append(['', '']) 1802 if document_part['seq_idx'] is None: 1803 items.append([_('Part'), '#%s' % document_part['pk_obj']]) 1804 else: 1805 items.append([_('Part'), '%s [#%s]' % (document_part['seq_idx'], document_part['pk_obj'])]) 1806 if document_part['obj_comment'] is not None: 1807 items.append([_('Comment'), document_part['obj_comment']]) 1808 if document_part['filename'] is not None: 1809 items.append([_('Filename'), document_part['filename']]) 1810 items.append([_('Data size'), gmTools.size2str(document_part['size'])]) 1811 review_parts = [] 1812 if document_part['reviewed_by_you']: 1813 review_parts.append(_('by you')) 1814 if document_part['reviewed_by_intended_reviewer']: 1815 review_parts.append(_('by intended reviewer')) 1816 review = ', '.join(review_parts) 1817 if review == '': 1818 review = gmTools.u_diameter 1819 items.append([_('Reviewed'), review]) 1820 #items.append([_(u'Reviewed'), gmTools.bool2subst(document_part['reviewed'], review, u'', u'?')]) 1821 return items
1822 1823 #============================================================
1824 -class cDocTree(wx.TreeCtrl, gmRegetMixin.cRegetOnPaintMixin, treemixin.ExpansionState):
1825 """This wx.TreeCtrl derivative displays a tree view of stored medical documents. 1826 1827 It listens to document and patient changes and updates itself accordingly. 1828 1829 This acts on the current patient. 1830 """ 1831 _sort_modes = ['age', 'review', 'episode', 'type', 'issue', 'org'] 1832 _root_node_labels = None 1833 1834 #--------------------------------------------------------
1835 - def __init__(self, parent, id, *args, **kwds):
1836 """Set up our specialised tree. 1837 """ 1838 kwds['style'] = wx.TR_NO_BUTTONS | wx.NO_BORDER | wx.TR_SINGLE 1839 wx.TreeCtrl.__init__(self, parent, id, *args, **kwds) 1840 1841 gmRegetMixin.cRegetOnPaintMixin.__init__(self) 1842 1843 tmp = _('available documents (%s)') 1844 unsigned = _('unsigned (%s) on top') % '\u270D' 1845 cDocTree._root_node_labels = { 1846 'age': tmp % _('most recent on top'), 1847 'review': tmp % unsigned, 1848 'episode': tmp % _('sorted by episode'), 1849 'issue': tmp % _('sorted by health issue'), 1850 'type': tmp % _('sorted by type'), 1851 'org': tmp % _('sorted by organization') 1852 } 1853 1854 self.root = None 1855 self.__sort_mode = 'age' 1856 1857 self.__expanded_nodes = None 1858 self.__show_details_callback = None 1859 1860 self.__build_context_menus() 1861 self.__register_interests() 1862 self._schedule_data_reget()
1863 1864 #-------------------------------------------------------- 1865 # external API 1866 #--------------------------------------------------------
1867 - def display_selected_part(self, *args, **kwargs):
1868 1869 node = self.GetSelection() 1870 node_data = self.GetItemData(node) 1871 1872 if not isinstance(node_data, gmDocuments.cDocumentPart): 1873 return True 1874 1875 self.__display_part(part = node_data) 1876 return True
1877 1878 #-------------------------------------------------------- 1879 # properties 1880 #--------------------------------------------------------
1881 - def _get_sort_mode(self):
1882 return self.__sort_mode
1883
1884 - def _set_sort_mode(self, mode):
1885 if mode is None: 1886 mode = 'age' 1887 1888 if mode == self.__sort_mode: 1889 return 1890 1891 if mode not in cDocTree._sort_modes: 1892 raise ValueError('invalid document tree sort mode [%s], valid modes: %s' % (mode, cDocTree._sort_modes)) 1893 1894 self.__sort_mode = mode 1895 self.__expanded_nodes = None 1896 1897 curr_pat = gmPerson.gmCurrentPatient() 1898 if not curr_pat.connected: 1899 return 1900 1901 self._schedule_data_reget()
1902 1903 sort_mode = property(_get_sort_mode, _set_sort_mode) 1904 1905 #--------------------------------------------------------
1906 - def _set_show_details_callback(self, callback):
1907 if callback is not None: 1908 if not callable(callback): 1909 raise ValueError('<%s> is not callable') 1910 self.__show_details_callback = callback
1911 1912 show_details_callback = property(lambda x:x, _set_show_details_callback) 1913 1914 #-------------------------------------------------------- 1915 # reget-on-paint API 1916 #--------------------------------------------------------
1917 - def _populate_with_data(self):
1918 curr_pat = gmPerson.gmCurrentPatient() 1919 if not curr_pat.connected: 1920 gmDispatcher.send(signal = 'statustext', msg = _('Cannot load documents. No active patient.')) 1921 return False 1922 1923 if not self.__populate_tree(): 1924 return False 1925 1926 return True
1927 1928 #-------------------------------------------------------- 1929 # internal helpers 1930 #--------------------------------------------------------
1931 - def __register_interests(self):
1932 self.Bind(wx.EVT_TREE_SEL_CHANGED, self._on_tree_item_selected) 1933 self.Bind(wx.EVT_TREE_ITEM_ACTIVATED, self._on_activate) 1934 self.Bind(wx.EVT_TREE_ITEM_MENU, self._on_tree_item_context_menu) 1935 self.Bind(wx.EVT_TREE_ITEM_GETTOOLTIP, self._on_tree_item_gettooltip) 1936 1937 gmDispatcher.connect(signal = 'pre_patient_unselection', receiver = self._on_pre_patient_unselection) 1938 gmDispatcher.connect(signal = 'post_patient_selection', receiver = self._on_post_patient_selection) 1939 gmDispatcher.connect(signal = 'blobs.doc_med_mod_db', receiver = self._on_doc_mod_db) 1940 gmDispatcher.connect(signal = 'blobs.doc_obj_mod_db', receiver = self._on_doc_page_mod_db)
1941 1942 #--------------------------------------------------------
1943 - def __build_context_menus(self):
1944 1945 # --- part context menu --- 1946 self.__part_context_menu = wx.Menu(title = _('Part Actions:')) 1947 1948 item = self.__part_context_menu.Append(-1, _('Display part')) 1949 self.Bind(wx.EVT_MENU, self.__display_curr_part, item) 1950 item = self.__part_context_menu.Append(-1, _('%s Sign/Edit properties') % '\u270D') 1951 self.Bind(wx.EVT_MENU, self.__review_curr_part, item) 1952 1953 self.__part_context_menu.AppendSeparator() 1954 1955 item = self.__part_context_menu.Append(-1, _('Delete part')) 1956 self.Bind(wx.EVT_MENU, self.__delete_part, item, item) 1957 item = self.__part_context_menu.Append(-1, _('Move part')) 1958 self.Bind(wx.EVT_MENU, self.__move_part, item) 1959 item = self.__part_context_menu.Append(-1, _('Print part')) 1960 self.Bind(wx.EVT_MENU, self.__print_part, item) 1961 item = self.__part_context_menu.Append(-1, _('Fax part')) 1962 self.Bind(wx.EVT_MENU, self.__fax_part, item) 1963 item = self.__part_context_menu.Append(-1, _('Mail part')) 1964 self.Bind(wx.EVT_MENU, self.__mail_part, item) 1965 item = self.__part_context_menu.Append(-1, _('Save part to disk')) 1966 self.Bind(wx.EVT_MENU, self.__save_part_to_disk, item) 1967 1968 self.__part_context_menu.AppendSeparator() # so we can append more items 1969 1970 # --- doc context menu --- 1971 self.__doc_context_menu = wx.Menu(title = _('Document Actions:')) 1972 1973 item = self.__doc_context_menu.Append(-1, _('%s Sign/Edit properties') % '\u270D') 1974 self.Bind(wx.EVT_MENU, self.__review_curr_part, item) 1975 item = self.__doc_context_menu.Append(-1, _('Delete document')) 1976 self.Bind(wx.EVT_MENU, self.__delete_document, item) 1977 1978 self.__doc_context_menu.AppendSeparator() 1979 1980 item = self.__doc_context_menu.Append(-1, _('Add parts')) 1981 self.Bind(wx.EVT_MENU, self.__add_part, item) 1982 item = self.__doc_context_menu.Append(-1, _('Add part from clipboard')) 1983 self.Bind(wx.EVT_MENU, self.__add_part_from_clipboard, item, item) 1984 item = self.__doc_context_menu.Append(-1, _('Print all parts')) 1985 self.Bind(wx.EVT_MENU, self.__print_doc, item) 1986 item = self.__doc_context_menu.Append(-1, _('Fax all parts')) 1987 self.Bind(wx.EVT_MENU, self.__fax_doc, item) 1988 item = self.__doc_context_menu.Append(-1, _('Mail all parts')) 1989 self.Bind(wx.EVT_MENU, self.__mail_doc, item) 1990 item = self.__doc_context_menu.Append(-1, _('Save all parts to disk')) 1991 self.Bind(wx.EVT_MENU, self.__save_doc_to_disk, item) 1992 item = self.__doc_context_menu.Append(-1, _('Copy all parts to export area')) 1993 self.Bind(wx.EVT_MENU, self.__copy_doc_to_export_area, item) 1994 1995 self.__doc_context_menu.AppendSeparator() 1996 1997 item = self.__doc_context_menu.Append(-1, _('Access external original')) 1998 self.Bind(wx.EVT_MENU, self.__access_external_original, item) 1999 item = self.__doc_context_menu.Append(-1, _('Edit corresponding encounter')) 2000 self.Bind(wx.EVT_MENU, self.__edit_encounter_details, item) 2001 item = self.__doc_context_menu.Append(-1, _('Select corresponding encounter')) 2002 self.Bind(wx.EVT_MENU, self.__select_encounter, item) 2003 item = self.__doc_context_menu.Append(-1, _('Manage descriptions')) 2004 self.Bind(wx.EVT_MENU, self.__manage_document_descriptions, item)
2005 2006 # document / description 2007 # self.__desc_menu = wx.Menu() 2008 # item = self.__doc_context_menu.Append(-1, _('Descriptions ...'), self.__desc_menu) 2009 # item = self.__desc_menu.Append(-1, _('Add new description')) 2010 # self.Bind(wx.EVT_MENU, self.__desc_menu, self.__add_doc_desc, item) 2011 # item = self.__desc_menu.Append(-1, _('Delete description')) 2012 # self.Bind(wx.EVT_MENU, self.__desc_menu, self.__del_doc_desc, item) 2013 # self.__desc_menu.AppendSeparator() 2014 2015 #--------------------------------------------------------
2016 - def __populate_tree(self):
2017 2018 wx.BeginBusyCursor() 2019 2020 # clean old tree 2021 if self.root is not None: 2022 self.DeleteAllItems() 2023 2024 # init new tree 2025 self.root = self.AddRoot(cDocTree._root_node_labels[self.__sort_mode], -1, -1) 2026 self.SetItemData(self.root, None) 2027 self.SetItemHasChildren(self.root, False) 2028 2029 # read documents from database 2030 curr_pat = gmPerson.gmCurrentPatient() 2031 docs_folder = curr_pat.get_document_folder() 2032 docs = docs_folder.get_documents() 2033 2034 if docs is None: 2035 gmGuiHelpers.gm_show_error ( 2036 aMessage = _('Error searching documents.'), 2037 aTitle = _('loading document list') 2038 ) 2039 # avoid recursion of GUI updating 2040 wx.EndBusyCursor() 2041 return True 2042 2043 if len(docs) == 0: 2044 wx.EndBusyCursor() 2045 return True 2046 2047 # fill new tree from document list 2048 self.SetItemHasChildren(self.root, True) 2049 2050 # add our documents as first level nodes 2051 intermediate_nodes = {} 2052 for doc in docs: 2053 2054 parts = doc.parts 2055 2056 if len(parts) == 0: 2057 no_parts = _('no parts') 2058 elif len(parts) == 1: 2059 no_parts = _('1 part') 2060 else: 2061 no_parts = _('%s parts') % len(parts) 2062 2063 # need intermediate branch level ? 2064 if self.__sort_mode == 'episode': 2065 intermediate_label = '%s%s' % (doc['episode'], gmTools.coalesce(doc['health_issue'], '', ' (%s)')) 2066 doc_label = _('%s%7s %s:%s (%s)') % ( 2067 gmTools.bool2subst(doc.has_unreviewed_parts, gmTools.u_writing_hand, '', '?'), 2068 gmDateTime.pydt_strftime(doc['clin_when'], '%m/%Y'), 2069 doc['l10n_type'][:26], 2070 gmTools.coalesce(value2test = doc['comment'], return_instead = '', template4value = ' %s'), 2071 no_parts 2072 ) 2073 if intermediate_label not in intermediate_nodes: 2074 intermediate_nodes[intermediate_label] = self.AppendItem(parent = self.root, text = intermediate_label) 2075 self.SetItemBold(intermediate_nodes[intermediate_label], bold = True) 2076 self.SetItemData(intermediate_nodes[intermediate_label], {'pk_episode': doc['pk_episode']}) 2077 self.SetItemHasChildren(intermediate_nodes[intermediate_label], True) 2078 parent = intermediate_nodes[intermediate_label] 2079 2080 elif self.__sort_mode == 'type': 2081 intermediate_label = doc['l10n_type'] 2082 doc_label = _('%s%7s (%s):%s') % ( 2083 gmTools.bool2subst(doc.has_unreviewed_parts, gmTools.u_writing_hand, '', '?'), 2084 gmDateTime.pydt_strftime(doc['clin_when'], '%m/%Y'), 2085 no_parts, 2086 gmTools.coalesce(value2test = doc['comment'], return_instead = '', template4value = ' %s') 2087 ) 2088 if intermediate_label not in intermediate_nodes: 2089 intermediate_nodes[intermediate_label] = self.AppendItem(parent = self.root, text = intermediate_label) 2090 self.SetItemBold(intermediate_nodes[intermediate_label], bold = True) 2091 self.SetItemData(intermediate_nodes[intermediate_label], None) 2092 self.SetItemHasChildren(intermediate_nodes[intermediate_label], True) 2093 parent = intermediate_nodes[intermediate_label] 2094 2095 elif self.__sort_mode == 'issue': 2096 if doc['health_issue'] is None: 2097 intermediate_label = _('%s (unattributed episode)') % doc['episode'] 2098 else: 2099 intermediate_label = doc['health_issue'] 2100 doc_label = _('%s%7s %s:%s (%s)') % ( 2101 gmTools.bool2subst(doc.has_unreviewed_parts, gmTools.u_writing_hand, '', '?'), 2102 gmDateTime.pydt_strftime(doc['clin_when'], '%m/%Y'), 2103 doc['l10n_type'][:26], 2104 gmTools.coalesce(value2test = doc['comment'], return_instead = '', template4value = ' %s'), 2105 no_parts 2106 ) 2107 if intermediate_label not in intermediate_nodes: 2108 intermediate_nodes[intermediate_label] = self.AppendItem(parent = self.root, text = intermediate_label) 2109 self.SetItemBold(intermediate_nodes[intermediate_label], bold = True) 2110 self.SetItemData(intermediate_nodes[intermediate_label], {'pk_health_issue': doc['pk_health_issue']}) 2111 self.SetItemHasChildren(intermediate_nodes[intermediate_label], True) 2112 parent = intermediate_nodes[intermediate_label] 2113 2114 elif self.__sort_mode == 'org': 2115 if doc['pk_org'] is None: 2116 intermediate_label = _('unknown organization') 2117 else: 2118 if doc['unit_is_receiver']: 2119 direction = _('to: %s') 2120 else: 2121 direction = _('from: %s') 2122 # this praxis ? 2123 if doc['pk_org'] == gmPraxis.gmCurrentPraxisBranch()['pk_org']: 2124 org_str = _('this praxis') 2125 else: 2126 org_str = doc['organization'] 2127 intermediate_label = direction % org_str 2128 doc_label = _('%s%7s %s:%s (%s)') % ( 2129 gmTools.bool2subst(doc.has_unreviewed_parts, gmTools.u_writing_hand, '', '?'), 2130 gmDateTime.pydt_strftime(doc['clin_when'], '%m/%Y'), 2131 doc['l10n_type'][:26], 2132 gmTools.coalesce(value2test = doc['comment'], return_instead = '', template4value = ' %s'), 2133 no_parts 2134 ) 2135 if intermediate_label not in intermediate_nodes: 2136 intermediate_nodes[intermediate_label] = self.AppendItem(parent = self.root, text = intermediate_label) 2137 self.SetItemBold(intermediate_nodes[intermediate_label], bold = True) 2138 # not quite right: always shows data of the _last_ document of _any_ org unit of this org 2139 self.SetItemData(intermediate_nodes[intermediate_label], doc.org_unit) 2140 self.SetItemHasChildren(intermediate_nodes[intermediate_label], True) 2141 parent = intermediate_nodes[intermediate_label] 2142 2143 elif self.__sort_mode == 'age': 2144 intermediate_label = gmDateTime.pydt_strftime(doc['clin_when'], '%Y') 2145 doc_label = _('%s%7s %s:%s (%s)') % ( 2146 gmTools.bool2subst(doc.has_unreviewed_parts, gmTools.u_writing_hand, '', '?'), 2147 gmDateTime.pydt_strftime(doc['clin_when'], '%b %d'), 2148 doc['l10n_type'][:26], 2149 gmTools.coalesce(value2test = doc['comment'], return_instead = '', template4value = ' %s'), 2150 no_parts 2151 ) 2152 if intermediate_label not in intermediate_nodes: 2153 intermediate_nodes[intermediate_label] = self.AppendItem(parent = self.root, text = intermediate_label) 2154 self.SetItemBold(intermediate_nodes[intermediate_label], bold = True) 2155 self.SetItemData(intermediate_nodes[intermediate_label], doc['clin_when']) 2156 self.SetItemHasChildren(intermediate_nodes[intermediate_label], True) 2157 parent = intermediate_nodes[intermediate_label] 2158 2159 else: 2160 doc_label = _('%s%7s %s:%s (%s)') % ( 2161 gmTools.bool2subst(doc.has_unreviewed_parts, gmTools.u_writing_hand, '', '?'), 2162 gmDateTime.pydt_strftime(doc['clin_when'], '%Y-%m'), 2163 doc['l10n_type'][:26], 2164 gmTools.coalesce(value2test = doc['comment'], return_instead = '', template4value = ' %s'), 2165 no_parts 2166 ) 2167 parent = self.root 2168 2169 doc_node = self.AppendItem(parent = parent, text = doc_label) 2170 #self.SetItemBold(doc_node, bold = True) 2171 self.SetItemData(doc_node, doc) 2172 if len(parts) == 0: 2173 self.SetItemHasChildren(doc_node, False) 2174 else: 2175 self.SetItemHasChildren(doc_node, True) 2176 2177 # now add parts as child nodes 2178 for part in parts: 2179 f_ext = '' 2180 if part['filename'] is not None: 2181 f_ext = os.path.splitext(part['filename'])[1].strip('.').strip() 2182 if f_ext != '': 2183 f_ext = ' .' + f_ext.upper() 2184 label = '%s%s (%s%s)%s' % ( 2185 gmTools.bool2str ( 2186 boolean = part['reviewed'] or part['reviewed_by_you'] or part['reviewed_by_intended_reviewer'], 2187 true_str = '', 2188 false_str = gmTools.u_writing_hand 2189 ), 2190 _('part %2s') % part['seq_idx'], 2191 gmTools.size2str(part['size']), 2192 f_ext, 2193 gmTools.coalesce ( 2194 part['obj_comment'], 2195 '', 2196 ': %s%%s%s' % (gmTools.u_left_double_angle_quote, gmTools.u_right_double_angle_quote) 2197 ) 2198 ) 2199 2200 part_node = self.AppendItem(parent = doc_node, text = label) 2201 self.SetItemData(part_node, part) 2202 self.SetItemHasChildren(part_node, False) 2203 2204 self.__sort_nodes() 2205 self.SelectItem(self.root) 2206 2207 # restore expansion state 2208 if self.__expanded_nodes is not None: 2209 self.ExpansionState = self.__expanded_nodes 2210 # but always expand root node 2211 self.Expand(self.root) 2212 # if no expansion state available then 2213 # expand intermediate nodes as well 2214 if self.__expanded_nodes is None: 2215 # but only if there are any 2216 if self.__sort_mode in ['episode', 'type', 'issue', 'org']: 2217 for key in intermediate_nodes.keys(): 2218 self.Expand(intermediate_nodes[key]) 2219 2220 wx.EndBusyCursor() 2221 2222 return True
2223 2224 #------------------------------------------------------------------------
2225 - def OnCompareItems (self, node1=None, node2=None):
2226 """Used in sorting items. 2227 2228 -1: 1 < 2 2229 0: 1 = 2 2230 1: 1 > 2 2231 """ 2232 # Windows can send bogus events so ignore that 2233 if not node1: 2234 _log.debug('invalid node 1') 2235 return 0 2236 if not node2: 2237 _log.debug('invalid node 2') 2238 return 0 2239 if not node1.IsOk(): 2240 _log.debug('no data on node 1') 2241 return 0 2242 if not node2.IsOk(): 2243 _log.debug('no data on node 2') 2244 return 0 2245 2246 data1 = self.GetItemData(node1) 2247 data2 = self.GetItemData(node2) 2248 2249 # doc node 2250 if isinstance(data1, gmDocuments.cDocument): 2251 date_field = 'clin_when' 2252 #date_field = 'modified_when' 2253 if self.__sort_mode == 'age': 2254 # reverse sort by date 2255 if data1[date_field] > data2[date_field]: 2256 return -1 2257 if data1[date_field] == data2[date_field]: 2258 return 0 2259 return 1 2260 if self.__sort_mode == 'episode': 2261 if data1['episode'] < data2['episode']: 2262 return -1 2263 if data1['episode'] == data2['episode']: 2264 # inner sort: reverse by date 2265 if data1[date_field] > data2[date_field]: 2266 return -1 2267 if data1[date_field] == data2[date_field]: 2268 return 0 2269 return 1 2270 return 1 2271 if self.__sort_mode == 'issue': 2272 if data1['health_issue'] == data2['health_issue']: 2273 # inner sort: reverse by date 2274 if data1[date_field] > data2[date_field]: 2275 return -1 2276 if data1[date_field] == data2[date_field]: 2277 return 0 2278 return 1 2279 if data1['health_issue'] < data2['health_issue']: 2280 return -1 2281 return 1 2282 if self.__sort_mode == 'review': 2283 # equality 2284 if data1.has_unreviewed_parts == data2.has_unreviewed_parts: 2285 # inner sort: reverse by date 2286 if data1[date_field] > data2[date_field]: 2287 return -1 2288 if data1[date_field] == data2[date_field]: 2289 return 0 2290 return 1 2291 if data1.has_unreviewed_parts: 2292 return -1 2293 return 1 2294 if self.__sort_mode == 'type': 2295 if data1['l10n_type'] < data2['l10n_type']: 2296 return -1 2297 if data1['l10n_type'] == data2['l10n_type']: 2298 # inner sort: reverse by date 2299 if data1[date_field] > data2[date_field]: 2300 return -1 2301 if data1[date_field] == data2[date_field]: 2302 return 0 2303 return 1 2304 return 1 2305 if self.__sort_mode == 'org': 2306 if (data1['organization'] is None) and (data2['organization'] is None): 2307 return 0 2308 if (data1['organization'] is None) and (data2['organization'] is not None): 2309 return 1 2310 if (data1['organization'] is not None) and (data2['organization'] is None): 2311 return -1 2312 txt1 = '%s %s' % (data1['organization'], data1['unit']) 2313 txt2 = '%s %s' % (data2['organization'], data2['unit']) 2314 if txt1 < txt2: 2315 return -1 2316 if txt1 == txt2: 2317 # inner sort: reverse by date 2318 if data1[date_field] > data2[date_field]: 2319 return -1 2320 if data1[date_field] == data2[date_field]: 2321 return 0 2322 return 1 2323 return 1 2324 2325 _log.error('unknown document sort mode [%s], reverse-sorting by age', self.__sort_mode) 2326 # reverse sort by date 2327 if data1[date_field] > data2[date_field]: 2328 return -1 2329 if data1[date_field] == data2[date_field]: 2330 return 0 2331 return 1 2332 2333 # part node 2334 if isinstance(data1, gmDocuments.cDocumentPart): 2335 # compare sequence IDs (= "page" numbers) 2336 # FIXME: wrong order ? 2337 if data1['seq_idx'] < data2['seq_idx']: 2338 return -1 2339 if data1['seq_idx'] == data2['seq_idx']: 2340 return 0 2341 return 1 2342 2343 # org unit node 2344 if isinstance(data1, gmOrganization.cOrgUnit): 2345 l1 = self.GetItemText(node1) 2346 l2 = self.GetItemText(node2) 2347 if l1 < l2: 2348 return -1 2349 if l1 == l2: 2350 return 0 2351 return 1 2352 2353 # episode or issue node 2354 if isinstance(data1, dict): 2355 if ('pk_episode' in data1) or ('pk_health_issue' in data1): 2356 l1 = self.GetItemText(node1) 2357 l2 = self.GetItemText(node2) 2358 if l1 < l2: 2359 return -1 2360 if l1 == l2: 2361 return 0 2362 return 1 2363 _log.error('dict but unknown content: %s', data1.keys()) 2364 return 1 2365 2366 # doc.year node 2367 if isinstance(data1, pydt.datetime): 2368 y1 = gmDateTime.pydt_strftime(data1, '%Y') 2369 y2 = gmDateTime.pydt_strftime(data2, '%Y') 2370 # reverse chronologically 2371 if y1 < y2: 2372 return 1 2373 if y1 == y2: 2374 return 0 2375 return -1 2376 2377 # type node 2378 # else sort alphabetically by label 2379 if None in [data1, data2]: 2380 l1 = self.GetItemText(node1) 2381 l2 = self.GetItemText(node2) 2382 if l1 < l2: 2383 return -1 2384 if l1 == l2: 2385 return 0 2386 else: 2387 if data1 < data2: 2388 return -1 2389 if data1 == data2: 2390 return 0 2391 return 1
2392 2393 #------------------------------------------------------------------------ 2394 # event handlers 2395 #------------------------------------------------------------------------
2396 - def _on_doc_mod_db(self, *args, **kwargs):
2397 self.__expanded_nodes = self.ExpansionState 2398 self._schedule_data_reget()
2399 2400 #------------------------------------------------------------------------
2401 - def _on_doc_page_mod_db(self, *args, **kwargs):
2402 self.__expanded_nodes = self.ExpansionState 2403 self._schedule_data_reget()
2404 2405 #------------------------------------------------------------------------
2406 - def _on_pre_patient_unselection(self, *args, **kwargs):
2407 # empty out tree 2408 if self.root is not None: 2409 self.DeleteAllItems() 2410 self.root = None
2411 2412 #------------------------------------------------------------------------
2413 - def _on_post_patient_selection(self, *args, **kwargs):
2414 # FIXME: self.__load_expansion_history_from_db (but not apply it !) 2415 self.__expanded_nodes = None 2416 self._schedule_data_reget()
2417 2418 #--------------------------------------------------------
2419 - def __update_details_view(self):
2420 if self.__curr_node_data is None: 2421 return 2422 2423 # pseudo root node or "type" 2424 if self.__curr_node_data is None: 2425 self.__show_details_callback(document = None, part = None) 2426 return 2427 2428 # document node 2429 if isinstance(self.__curr_node_data, gmDocuments.cDocument): 2430 self.__show_details_callback(document = self.__curr_node_data, part = None) 2431 return 2432 2433 if isinstance(self.__curr_node_data, gmDocuments.cDocumentPart): 2434 doc = self.GetItemData(self.GetItemParent(self.__curr_node)) 2435 self.__show_details_callback(document = doc, part = self.__curr_node_data) 2436 return 2437 2438 if isinstance(self.__curr_node_data, gmOrganization.cOrgUnit): 2439 self.__show_details_callback(org_unit = self.__curr_node_data) 2440 return 2441 2442 if isinstance(self.__curr_node_data, pydt.datetime): 2443 # could be getting some statistics about the year 2444 return 2445 2446 if isinstance(self.__curr_node_data, dict): 2447 _log.debug('node data is dict: %s', self.__curr_node_data) 2448 try: 2449 if self.__curr_node_data['pk_health_issue'] is None: 2450 _log.debug('node data dict holds pseudo-issue for unattributed episodes, ignoring') 2451 else: 2452 issue = gmEMRStructItems.cHealthIssue(aPK_obj = self.__curr_node_data['pk_health_issue']) 2453 except KeyError: 2454 issue = None 2455 try: 2456 episode = gmEMRStructItems.cEpisode(aPK_obj = self.__curr_node_data['pk_episode']) 2457 except KeyError: 2458 episode = None 2459 self.__show_details_callback(issue = issue, episode = episode) 2460 return 2461 2462 # # string nodes are labels such as episodes which may or may not have children 2463 # if isinstance(self.__curr_node_data, str): 2464 # self.__show_details_callback(document = None, part = None) 2465 # return 2466 2467 raise ValueError('invalid document tree node data type: %s' % type(self.__curr_node_data))
2468 2469 #--------------------------------------------------------
2470 - def _on_tree_item_selected(self, event):
2471 event.Skip() 2472 self.__curr_node = event.GetItem() 2473 self.__curr_node_data = self.GetItemData(self.__curr_node) 2474 self.__update_details_view()
2475 2476 #------------------------------------------------------------------------
2477 - def _on_activate(self, event):
2478 node = event.GetItem() 2479 node_data = self.GetItemData(node) 2480 2481 # exclude pseudo root node 2482 if node_data is None: 2483 return None 2484 2485 if isinstance(node_data, gmDocuments.cDocumentPart): 2486 self.__display_part(part = node_data) 2487 return True 2488 2489 event.Skip()
2490 2491 #--------------------------------------------------------
2492 - def _on_tree_item_context_menu(self, evt):
2493 2494 self.__curr_node_data = self.GetItemData(evt.Item) 2495 2496 # exclude pseudo root node 2497 if self.__curr_node_data is None: 2498 return None 2499 2500 # documents 2501 if isinstance(self.__curr_node_data, gmDocuments.cDocument): 2502 self.__handle_doc_context() 2503 2504 # parts 2505 if isinstance(self.__curr_node_data, gmDocuments.cDocumentPart): 2506 self.__handle_part_context() 2507 2508 del self.__curr_node_data 2509 evt.Skip()
2510 2511 #--------------------------------------------------------
2512 - def __activate_as_current_photo(self, evt):
2513 self.__curr_node_data.set_as_active_photograph()
2514 2515 #--------------------------------------------------------
2516 - def __display_curr_part(self, evt):
2517 self.__display_part(part = self.__curr_node_data)
2518 2519 #--------------------------------------------------------
2520 - def __review_curr_part(self, evt):
2521 self.__review_part(part = self.__curr_node_data)
2522 2523 #--------------------------------------------------------
2524 - def __manage_document_descriptions(self, evt):
2525 manage_document_descriptions(parent = self, document = self.__curr_node_data)
2526 2527 #--------------------------------------------------------
2528 - def _on_tree_item_gettooltip(self, event):
2529 2530 item = event.GetItem() 2531 2532 if not item.IsOk(): 2533 event.SetToolTip('') 2534 return 2535 2536 data = self.GetItemData(item) 2537 # documents, parts 2538 if isinstance(data, (gmDocuments.cDocument, gmDocuments.cDocumentPart)): 2539 tt = data.format() 2540 elif isinstance(data, gmOrganization.cOrgUnit): 2541 tt = '\n'.join(data.format(with_address = True, with_org = True, with_comms = True)) 2542 elif isinstance(data, dict): 2543 try: 2544 tt = data['tooltip'] 2545 except KeyError: 2546 tt = '' 2547 # # explicit tooltip strings 2548 # elif isinstance(data, str): 2549 # tt = data 2550 # # other (root, "None") 2551 else: 2552 tt = '' 2553 2554 event.SetToolTip(tt)
2555 2556 #-------------------------------------------------------- 2557 # internal API 2558 #--------------------------------------------------------
2559 - def __sort_nodes(self, start_node=None):
2560 2561 if start_node is None: 2562 start_node = self.GetRootItem() 2563 2564 # protect against empty tree where not even 2565 # a root node exists 2566 if not start_node.IsOk(): 2567 return True 2568 2569 self.SortChildren(start_node) 2570 2571 child_node, cookie = self.GetFirstChild(start_node) 2572 while child_node.IsOk(): 2573 self.__sort_nodes(start_node = child_node) 2574 child_node, cookie = self.GetNextChild(start_node, cookie) 2575 2576 return
2577 2578 #--------------------------------------------------------
2579 - def __handle_doc_context(self):
2580 self.PopupMenu(self.__doc_context_menu, wx.DefaultPosition)
2581 2582 #--------------------------------------------------------
2583 - def __handle_part_context(self):
2584 ID = None 2585 # make active patient photograph 2586 if self.__curr_node_data['type'] == 'patient photograph': 2587 item = self.__part_context_menu.Append(-1, _('Activate as current photo')) 2588 self.Bind(wx.EVT_MENU, self.__activate_as_current_photo, item) 2589 ID = item.Id 2590 2591 self.PopupMenu(self.__part_context_menu, wx.DefaultPosition) 2592 2593 if ID is not None: 2594 self.__part_context_menu.Delete(ID)
2595 2596 #-------------------------------------------------------- 2597 # part level context menu handlers 2598 #--------------------------------------------------------
2599 - def __display_part(self, part):
2600 """Display document part.""" 2601 2602 # sanity check 2603 if part['size'] == 0: 2604 _log.debug('cannot display part [%s] - 0 bytes', part['pk_obj']) 2605 gmGuiHelpers.gm_show_error ( 2606 aMessage = _('Document part does not seem to exist in database !'), 2607 aTitle = _('showing document') 2608 ) 2609 return None 2610 2611 wx.BeginBusyCursor() 2612 2613 cfg = gmCfg.cCfgSQL() 2614 2615 # determine database export chunk size 2616 chunksize = int( 2617 cfg.get2 ( 2618 option = "horstspace.blob_export_chunk_size", 2619 workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace, 2620 bias = 'workplace', 2621 default = default_chunksize 2622 )) 2623 2624 # shall we force blocking during view ? 2625 block_during_view = bool( cfg.get2 ( 2626 option = 'horstspace.document_viewer.block_during_view', 2627 workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace, 2628 bias = 'user', 2629 default = None 2630 )) 2631 2632 # display it 2633 successful, msg = part.display_via_mime ( 2634 chunksize = chunksize, 2635 block = block_during_view 2636 ) 2637 2638 wx.EndBusyCursor() 2639 2640 if not successful: 2641 gmGuiHelpers.gm_show_error ( 2642 aMessage = _('Cannot display document part:\n%s') % msg, 2643 aTitle = _('showing document') 2644 ) 2645 return None 2646 2647 # handle review after display 2648 # 0: never 2649 # 1: always 2650 # 2: if no review by myself exists yet 2651 # 3: if no review at all exists yet 2652 # 4: if no review by responsible reviewer 2653 review_after_display = int(cfg.get2 ( 2654 option = 'horstspace.document_viewer.review_after_display', 2655 workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace, 2656 bias = 'user', 2657 default = 3 2658 )) 2659 if review_after_display == 1: # always review 2660 self.__review_part(part=part) 2661 elif review_after_display == 2: # review if no review by me exists 2662 review_by_me = [ rev for rev in part.get_reviews() if rev['is_your_review'] ] 2663 if len(review_by_me) == 0: 2664 self.__review_part(part = part) 2665 elif review_after_display == 3: 2666 if len(part.get_reviews()) == 0: 2667 self.__review_part(part = part) 2668 elif review_after_display == 4: 2669 reviewed_by_responsible = [ rev for rev in part.get_reviews() if rev['is_review_by_responsible_reviewer'] ] 2670 if len(reviewed_by_responsible) == 0: 2671 self.__review_part(part = part) 2672 2673 return True
2674 #--------------------------------------------------------
2675 - def __review_part(self, part=None):
2676 dlg = cReviewDocPartDlg ( 2677 parent = self, 2678 id = -1, 2679 part = part 2680 ) 2681 dlg.ShowModal() 2682 dlg.DestroyLater()
2683 #--------------------------------------------------------
2684 - def __move_part(self, evt):
2685 target_doc = manage_documents ( 2686 parent = self, 2687 msg = _('\nSelect the document into which to move the selected part !\n') 2688 ) 2689 if target_doc is None: 2690 return 2691 if not self.__curr_node_data.reattach(pk_doc = target_doc['pk_doc']): 2692 gmGuiHelpers.gm_show_error ( 2693 aMessage = _('Cannot move document part.'), 2694 aTitle = _('Moving document part') 2695 )
2696 #--------------------------------------------------------
2697 - def __delete_part(self, evt):
2698 delete_it = gmGuiHelpers.gm_show_question ( 2699 cancel_button = True, 2700 title = _('Deleting document part'), 2701 question = _( 2702 'Are you sure you want to delete the %s part #%s\n' 2703 '\n' 2704 '%s' 2705 'from the following document\n' 2706 '\n' 2707 ' %s (%s)\n' 2708 '%s' 2709 '\n' 2710 'Really delete ?\n' 2711 '\n' 2712 '(this action cannot be reversed)' 2713 ) % ( 2714 gmTools.size2str(self.__curr_node_data['size']), 2715 self.__curr_node_data['seq_idx'], 2716 gmTools.coalesce(self.__curr_node_data['obj_comment'], '', ' "%s"\n\n'), 2717 self.__curr_node_data['l10n_type'], 2718 gmDateTime.pydt_strftime(self.__curr_node_data['date_generated'], format = '%Y-%m-%d', accuracy = gmDateTime.acc_days), 2719 gmTools.coalesce(self.__curr_node_data['doc_comment'], '', ' "%s"\n') 2720 ) 2721 ) 2722 if not delete_it: 2723 return 2724 2725 gmDocuments.delete_document_part ( 2726 part_pk = self.__curr_node_data['pk_obj'], 2727 encounter_pk = gmPerson.gmCurrentPatient().emr.active_encounter['pk_encounter'] 2728 )
2729 #--------------------------------------------------------
2730 - def __process_part(self, action=None, l10n_action=None):
2731 2732 gmHooks.run_hook_script(hook = 'before_%s_doc_part' % action) 2733 2734 wx.BeginBusyCursor() 2735 2736 # detect wrapper 2737 found, external_cmd = gmShellAPI.detect_external_binary('gm-%s_doc' % action) 2738 if not found: 2739 found, external_cmd = gmShellAPI.detect_external_binary('gm-%s_doc.bat' % action) 2740 if not found: 2741 _log.error('neither of gm-%s_doc or gm-%s_doc.bat found', action, action) 2742 wx.EndBusyCursor() 2743 gmGuiHelpers.gm_show_error ( 2744 _('Cannot %(l10n_action)s document part - %(l10n_action)s command not found.\n' 2745 '\n' 2746 'Either of gm-%(action)s_doc or gm-%(action)s_doc.bat\n' 2747 'must be in the execution path. The command will\n' 2748 'be passed the filename to %(l10n_action)s.' 2749 ) % {'action': action, 'l10n_action': l10n_action}, 2750 _('Processing document part: %s') % l10n_action 2751 ) 2752 return 2753 2754 cfg = gmCfg.cCfgSQL() 2755 2756 # determine database export chunk size 2757 chunksize = int(cfg.get2 ( 2758 option = "horstspace.blob_export_chunk_size", 2759 workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace, 2760 bias = 'workplace', 2761 default = default_chunksize 2762 )) 2763 2764 part_file = self.__curr_node_data.save_to_file(aChunkSize = chunksize) 2765 2766 if action == 'print': 2767 cmd = '%s generic_document %s' % (external_cmd, part_file) 2768 else: 2769 cmd = '%s %s' % (external_cmd, part_file) 2770 if os.name == 'nt': 2771 blocking = True 2772 else: 2773 blocking = False 2774 success = gmShellAPI.run_command_in_shell ( 2775 command = cmd, 2776 blocking = blocking 2777 ) 2778 2779 wx.EndBusyCursor() 2780 2781 if not success: 2782 _log.error('%s command failed: [%s]', action, cmd) 2783 gmGuiHelpers.gm_show_error ( 2784 _('Cannot %(l10n_action)s document part - %(l10n_action)s command failed.\n' 2785 '\n' 2786 'You may need to check and fix either of\n' 2787 ' gm-%(action)s_doc (Unix/Mac) or\n' 2788 ' gm-%(action)s_doc.bat (Windows)\n' 2789 '\n' 2790 'The command is passed the filename to %(l10n_action)s.' 2791 ) % {'action': action, 'l10n_action': l10n_action}, 2792 _('Processing document part: %s') % l10n_action 2793 ) 2794 else: 2795 if action == 'mail': 2796 curr_pat = gmPerson.gmCurrentPatient() 2797 emr = curr_pat.emr 2798 emr.add_clin_narrative ( 2799 soap_cat = None, 2800 note = _('document part handed over to email program: %s') % self.__curr_node_data.format(single_line = True), 2801 episode = self.__curr_node_data['pk_episode'] 2802 )
2803 #--------------------------------------------------------
2804 - def __print_part(self, evt):
2805 self.__process_part(action = 'print', l10n_action = _('print'))
2806 #--------------------------------------------------------
2807 - def __fax_part(self, evt):
2808 self.__process_part(action = 'fax', l10n_action = _('fax'))
2809 #--------------------------------------------------------
2810 - def __mail_part(self, evt):
2811 self.__process_part(action = 'mail', l10n_action = _('mail'))
2812 #--------------------------------------------------------
2813 - def __save_part_to_disk(self, evt):
2814 """Save document part into directory.""" 2815 dlg = wx.DirDialog ( 2816 parent = self, 2817 message = _('Save document part to directory ...'), 2818 defaultPath = os.path.expanduser(os.path.join('~', 'gnumed')), 2819 style = wx.DD_DEFAULT_STYLE 2820 ) 2821 result = dlg.ShowModal() 2822 dirname = dlg.GetPath() 2823 dlg.DestroyLater() 2824 2825 if result != wx.ID_OK: 2826 return True 2827 2828 wx.BeginBusyCursor() 2829 2830 pat = gmPerson.gmCurrentPatient() 2831 fname = self.__curr_node_data.get_useful_filename ( 2832 patient = pat, 2833 make_unique = True, 2834 directory = dirname 2835 ) 2836 2837 cfg = gmCfg.cCfgSQL() 2838 2839 # determine database export chunk size 2840 chunksize = int(cfg.get2 ( 2841 option = "horstspace.blob_export_chunk_size", 2842 workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace, 2843 bias = 'workplace', 2844 default = default_chunksize 2845 )) 2846 2847 fname = self.__curr_node_data.save_to_file ( 2848 aChunkSize = chunksize, 2849 filename = fname, 2850 target_mime = None 2851 ) 2852 2853 wx.EndBusyCursor() 2854 2855 gmDispatcher.send(signal = 'statustext', msg = _('Successfully saved document part as [%s].') % fname) 2856 2857 return True
2858 2859 #-------------------------------------------------------- 2860 # document level context menu handlers 2861 #--------------------------------------------------------
2862 - def __select_encounter(self, evt):
2863 enc = gmEncounterWidgets.select_encounters ( 2864 parent = self, 2865 patient = gmPerson.gmCurrentPatient() 2866 ) 2867 if not enc: 2868 return 2869 self.__curr_node_data['pk_encounter'] = enc['pk_encounter'] 2870 self.__curr_node_data.save()
2871 #--------------------------------------------------------
2872 - def __edit_encounter_details(self, evt):
2873 enc = gmEMRStructItems.cEncounter(aPK_obj = self.__curr_node_data['pk_encounter']) 2874 gmEncounterWidgets.edit_encounter(parent = self, encounter = enc)
2875 #--------------------------------------------------------
2876 - def __process_doc(self, action=None, l10n_action=None):
2877 2878 gmHooks.run_hook_script(hook = 'before_%s_doc' % action) 2879 2880 wx.BeginBusyCursor() 2881 2882 # detect wrapper 2883 found, external_cmd = gmShellAPI.detect_external_binary('gm-%s_doc' % action) 2884 if not found: 2885 found, external_cmd = gmShellAPI.detect_external_binary('gm-%s_doc.bat' % action) 2886 if not found: 2887 _log.error('neither of gm-%s_doc or gm-%s_doc.bat found', action, action) 2888 wx.EndBusyCursor() 2889 gmGuiHelpers.gm_show_error ( 2890 _('Cannot %(l10n_action)s document - %(l10n_action)s command not found.\n' 2891 '\n' 2892 'Either of gm-%(action)s_doc or gm-%(action)s_doc.bat\n' 2893 'must be in the execution path. The command will\n' 2894 'be passed a list of filenames to %(l10n_action)s.' 2895 ) % {'action': action, 'l10n_action': l10n_action}, 2896 _('Processing document: %s') % l10n_action 2897 ) 2898 return 2899 2900 cfg = gmCfg.cCfgSQL() 2901 2902 # determine database export chunk size 2903 chunksize = int(cfg.get2 ( 2904 option = "horstspace.blob_export_chunk_size", 2905 workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace, 2906 bias = 'workplace', 2907 default = default_chunksize 2908 )) 2909 2910 part_files = self.__curr_node_data.save_parts_to_files(chunksize = chunksize) 2911 2912 if os.name == 'nt': 2913 blocking = True 2914 else: 2915 blocking = False 2916 2917 if action == 'print': 2918 cmd = '%s %s %s' % ( 2919 external_cmd, 2920 'generic_document', 2921 ' '.join(part_files) 2922 ) 2923 else: 2924 cmd = external_cmd + ' ' + ' '.join(part_files) 2925 success = gmShellAPI.run_command_in_shell ( 2926 command = cmd, 2927 blocking = blocking 2928 ) 2929 2930 wx.EndBusyCursor() 2931 2932 if not success: 2933 _log.error('%s command failed: [%s]', action, cmd) 2934 gmGuiHelpers.gm_show_error ( 2935 _('Cannot %(l10n_action)s document - %(l10n_action)s command failed.\n' 2936 '\n' 2937 'You may need to check and fix either of\n' 2938 ' gm-%(action)s_doc (Unix/Mac) or\n' 2939 ' gm-%(action)s_doc.bat (Windows)\n' 2940 '\n' 2941 'The command is passed a list of filenames to %(l10n_action)s.' 2942 ) % {'action': action, 'l10n_action': l10n_action}, 2943 _('Processing document: %s') % l10n_action 2944 )
2945 2946 #--------------------------------------------------------
2947 - def __print_doc(self, evt):
2948 self.__process_doc(action = 'print', l10n_action = _('print'))
2949 2950 #--------------------------------------------------------
2951 - def __fax_doc(self, evt):
2952 self.__process_doc(action = 'fax', l10n_action = _('fax'))
2953 2954 #--------------------------------------------------------
2955 - def __mail_doc(self, evt):
2956 self.__process_doc(action = 'mail', l10n_action = _('mail'))
2957 2958 #--------------------------------------------------------
2959 - def __add_part(self, evt):
2960 dlg = wx.FileDialog ( 2961 parent = self, 2962 message = _('Choose a file'), 2963 defaultDir = os.path.expanduser(os.path.join('~', 'gnumed')), 2964 defaultFile = '', 2965 wildcard = "%s (*)|*|PNGs (*.png)|*.png|PDFs (*.pdf)|*.pdf|TIFFs (*.tif)|*.tif|JPEGs (*.jpg)|*.jpg|%s (*.*)|*.*" % (_('all files'), _('all files (Win)')), 2966 style = wx.FD_OPEN | wx.FD_FILE_MUST_EXIST | wx.FD_MULTIPLE 2967 ) 2968 result = dlg.ShowModal() 2969 if result != wx.ID_CANCEL: 2970 self.__curr_node_data.add_parts_from_files(files = dlg.GetPaths(), reviewer = gmStaff.gmCurrentProvider()['pk_staff']) 2971 dlg.DestroyLater()
2972 2973 #--------------------------------------------------------
2974 - def __add_part_from_clipboard(self, evt):
2975 clip = gmGuiHelpers.clipboard2file() 2976 if clip is None: 2977 return 2978 if clip is False: 2979 return 2980 gmMimeLib.call_viewer_on_file(clip, block = False) 2981 really_add = gmGuiHelpers.gm_show_question ( 2982 question = _('Really add the displayed clipboard item into the document ?'), 2983 title = _('Document part from clipboard') 2984 ) 2985 if not really_add: 2986 return 2987 self.__curr_node_data.add_parts_from_files(files = [clip], reviewer = gmStaff.gmCurrentProvider()['pk_staff'])
2988 #--------------------------------------------------------
2989 - def __access_external_original(self, evt):
2990 2991 gmHooks.run_hook_script(hook = 'before_external_doc_access') 2992 2993 wx.BeginBusyCursor() 2994 2995 # detect wrapper 2996 found, external_cmd = gmShellAPI.detect_external_binary('gm_access_external_doc.sh') 2997 if not found: 2998 found, external_cmd = gmShellAPI.detect_external_binary('gm_access_external_doc.bat') 2999 if not found: 3000 _log.error('neither of gm_access_external_doc.sh or .bat found') 3001 wx.EndBusyCursor() 3002 gmGuiHelpers.gm_show_error ( 3003 _('Cannot access external document - access command not found.\n' 3004 '\n' 3005 'Either of gm_access_external_doc.sh or *.bat must be\n' 3006 'in the execution path. The command will be passed the\n' 3007 'document type and the reference URL for processing.' 3008 ), 3009 _('Accessing external document') 3010 ) 3011 return 3012 3013 cmd = '%s "%s" "%s"' % (external_cmd, self.__curr_node_data['type'], self.__curr_node_data['ext_ref']) 3014 if os.name == 'nt': 3015 blocking = True 3016 else: 3017 blocking = False 3018 success = gmShellAPI.run_command_in_shell ( 3019 command = cmd, 3020 blocking = blocking 3021 ) 3022 3023 wx.EndBusyCursor() 3024 3025 if not success: 3026 _log.error('External access command failed: [%s]', cmd) 3027 gmGuiHelpers.gm_show_error ( 3028 _('Cannot access external document - access command failed.\n' 3029 '\n' 3030 'You may need to check and fix either of\n' 3031 ' gm_access_external_doc.sh (Unix/Mac) or\n' 3032 ' gm_access_external_doc.bat (Windows)\n' 3033 '\n' 3034 'The command is passed the document type and the\n' 3035 'external reference URL on the command line.' 3036 ), 3037 _('Accessing external document') 3038 )
3039 #--------------------------------------------------------
3040 - def __save_doc_to_disk(self, evt):
3041 """Save document into directory. 3042 3043 - one file per object 3044 - into subdirectory named after patient 3045 """ 3046 pat = gmPerson.gmCurrentPatient() 3047 def_dir = os.path.expanduser(os.path.join('~', 'gnumed', pat.subdir_name)) 3048 gmTools.mkdir(def_dir) 3049 3050 dlg = wx.DirDialog ( 3051 parent = self, 3052 message = _('Save document into directory ...'), 3053 defaultPath = def_dir, 3054 style = wx.DD_DEFAULT_STYLE 3055 ) 3056 result = dlg.ShowModal() 3057 dirname = dlg.GetPath() 3058 dlg.DestroyLater() 3059 3060 if result != wx.ID_OK: 3061 return True 3062 3063 wx.BeginBusyCursor() 3064 3065 cfg = gmCfg.cCfgSQL() 3066 3067 # determine database export chunk size 3068 chunksize = int(cfg.get2 ( 3069 option = "horstspace.blob_export_chunk_size", 3070 workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace, 3071 bias = 'workplace', 3072 default = default_chunksize 3073 )) 3074 3075 fnames = self.__curr_node_data.save_parts_to_files(export_dir = dirname, chunksize = chunksize) 3076 3077 wx.EndBusyCursor() 3078 3079 gmDispatcher.send(signal='statustext', msg=_('Successfully saved %s parts into the directory [%s].') % (len(fnames), dirname)) 3080 3081 return True
3082 3083 #--------------------------------------------------------
3084 - def __copy_doc_to_export_area(self, evt):
3085 gmPerson.gmCurrentPatient().export_area.add_documents(documents = [self.__curr_node_data])
3086 3087 #--------------------------------------------------------
3088 - def __delete_document(self, evt):
3089 delete_it = gmGuiHelpers.gm_show_question ( 3090 aMessage = _('Are you sure you want to delete the document ?'), 3091 aTitle = _('Deleting document') 3092 ) 3093 if delete_it is True: 3094 curr_pat = gmPerson.gmCurrentPatient() 3095 emr = curr_pat.emr 3096 enc = emr.active_encounter 3097 gmDocuments.delete_document(document_id = self.__curr_node_data['pk_doc'], encounter_id = enc['pk_encounter'])
3098 3099 #============================================================ 3100 #============================================================ 3101 # PACS 3102 #============================================================ 3103 from Gnumed.wxGladeWidgets.wxgPACSPluginPnl import wxgPACSPluginPnl 3104
3105 -class cPACSPluginPnl(wxgPACSPluginPnl, gmRegetMixin.cRegetOnPaintMixin):
3106
3107 - def __init__(self, *args, **kwargs):
3108 wxgPACSPluginPnl.__init__(self, *args, **kwargs) 3109 gmRegetMixin.cRegetOnPaintMixin.__init__(self) 3110 self.__pacs = None 3111 self.__patient = gmPerson.gmCurrentPatient() 3112 self.__orthanc_patient = None 3113 self.__image_data = None 3114 3115 self.__init_ui() 3116 self.__register_interests()
3117 3118 #-------------------------------------------------------- 3119 # internal helpers 3120 #--------------------------------------------------------
3121 - def __init_ui(self):
3122 3123 login = gmPG2.get_default_login() 3124 self._TCTRL_host.Value = gmTools.coalesce(login.host, 'localhost') 3125 self._TCTRL_port.Value = '8042' 3126 3127 self._LCTRL_studies.set_columns(columns = [_('Date'), _('Description'), _('Organization'), _('Authority')]) 3128 self._LCTRL_studies.select_callback = self._on_studies_list_item_selected 3129 self._LCTRL_studies.deselect_callback = self._on_studies_list_item_deselected 3130 3131 self._LCTRL_series.set_columns(columns = [_('Time'), _('Method'), _('Body part'), _('Description')]) 3132 self._LCTRL_series.select_callback = self._on_series_list_item_selected 3133 self._LCTRL_series.deselect_callback = self._on_series_list_item_deselected 3134 3135 self._LCTRL_details.set_columns(columns = [_('DICOM field'), _('Value')]) 3136 self._LCTRL_details.set_column_widths() 3137 3138 self._BMP_preview.SetBitmap(wx.Bitmap.FromRGBA(50,50, red=0, green=0, blue=0, alpha = wx.ALPHA_TRANSPARENT)) 3139 3140 # pre-make thumbnail context menu 3141 self.__thumbnail_menu = wx.Menu() 3142 item = self.__thumbnail_menu.Append(-1, _('Show in DICOM viewer')) 3143 self.Bind(wx.EVT_MENU, self._on_show_image_as_dcm, item) 3144 item = self.__thumbnail_menu.Append(-1, _('Show in image viewer')) 3145 self.Bind(wx.EVT_MENU, self._on_show_image_as_png, item) 3146 self.__thumbnail_menu.AppendSeparator() 3147 item = self.__thumbnail_menu.Append(-1, _('Copy to export area')) 3148 self.Bind(wx.EVT_MENU, self._on_copy_image_to_export_area, item) 3149 item = self.__thumbnail_menu.Append(-1, _('Save as DICOM file (.dcm)')) 3150 self.Bind(wx.EVT_MENU, self._on_save_image_as_dcm, item) 3151 item = self.__thumbnail_menu.Append(-1, _('Save as image file (.png)')) 3152 self.Bind(wx.EVT_MENU, self._on_save_image_as_png, item) 3153 3154 # pre-make studies context menu 3155 self.__studies_menu = wx.Menu('Studies:') 3156 self.__studies_menu.AppendSeparator() 3157 item = self.__studies_menu.Append(-1, _('Show in DICOM viewer')) 3158 self.Bind(wx.EVT_MENU, self._on_studies_show_button_pressed, item) 3159 self.__studies_menu.AppendSeparator() 3160 # export 3161 item = self.__studies_menu.Append(-1, _('Selected into export area')) 3162 self.Bind(wx.EVT_MENU, self._on_copy_selected_studies_to_export_area, item) 3163 item = self.__studies_menu.Append(-1, _('ZIP of selected into export area')) 3164 self.Bind(wx.EVT_MENU, self._on_copy_zip_of_selected_studies_to_export_area, item) 3165 item = self.__studies_menu.Append(-1, _('All into export area')) 3166 self.Bind(wx.EVT_MENU, self._on_copy_all_studies_to_export_area, item) 3167 item = self.__studies_menu.Append(-1, _('ZIP of all into export area')) 3168 self.Bind(wx.EVT_MENU, self._on_copy_zip_of_all_studies_to_export_area, item) 3169 self.__studies_menu.AppendSeparator() 3170 # save 3171 item = self.__studies_menu.Append(-1, _('Save selected')) 3172 self.Bind(wx.EVT_MENU, self._on_save_selected_studies, item) 3173 item = self.__studies_menu.Append(-1, _('Save ZIP of selected')) 3174 self.Bind(wx.EVT_MENU, self._on_save_zip_of_selected_studies, item) 3175 item = self.__studies_menu.Append(-1, _('Save all')) 3176 self.Bind(wx.EVT_MENU, self._on_save_all_studies, item) 3177 item = self.__studies_menu.Append(-1, _('Save ZIP of all')) 3178 self.Bind(wx.EVT_MENU, self._on_save_zip_of_all_studies, item) 3179 self.__studies_menu.AppendSeparator() 3180 # dicomize 3181 item = self.__studies_menu.Append(-1, _('Add PDF to study')) 3182 self.Bind(wx.EVT_MENU, self._on_add_pdf_to_study, item)
3183 3184 #--------------------------------------------------------
3185 - def __set_button_states(self):
3186 # disable all buttons 3187 # server 3188 self._BTN_browse_pacs.Disable() 3189 self._BTN_upload.Disable() 3190 self._BTN_modify_orthanc_content.Disable() 3191 # patient (= all studies of patient) 3192 self._BTN_browse_patient.Disable() 3193 self._BTN_verify_patient_data.Disable() 3194 # study 3195 self._BTN_browse_study.Disable() 3196 self._BTN_studies_show.Disable() 3197 self._BTN_studies_export.Disable() 3198 # series 3199 # image 3200 self._BTN_image_show.Disable() 3201 self._BTN_image_export.Disable() 3202 self._BTN_previous_image.Disable() 3203 self._BTN_next_image.Disable() 3204 3205 if self.__pacs is None: 3206 return 3207 3208 # server buttons 3209 self._BTN_browse_pacs.Enable() 3210 self._BTN_upload.Enable() 3211 self._BTN_modify_orthanc_content.Enable() 3212 3213 if not self.__patient.connected: 3214 return 3215 3216 # patient buttons (= all studies of patient) 3217 self._BTN_verify_patient_data.Enable() 3218 if self.__orthanc_patient is not None: 3219 self._BTN_browse_patient.Enable() 3220 3221 if len(self._LCTRL_studies.selected_items) == 0: 3222 return 3223 3224 # study buttons 3225 self._BTN_browse_study.Enable() 3226 study_data = self._LCTRL_studies.get_selected_item_data(only_one = True) 3227 self._BTN_studies_show.Enable() 3228 self._BTN_studies_export.Enable() 3229 3230 if len(self._LCTRL_series.selected_items) == 0: 3231 return 3232 3233 series = self._LCTRL_series.get_selected_item_data(only_one = True) 3234 if len(series['instances']) == 0: 3235 return 3236 3237 # image buttons 3238 self._BTN_image_show.Enable() 3239 self._BTN_image_export.Enable() 3240 if len(series['instances']) > 1: 3241 self._BTN_previous_image.Enable() 3242 self._BTN_next_image.Enable()
3243 3244 #--------------------------------------------------------
3245 - def __reset_patient_data(self):
3246 self._LBL_patient_identification.SetLabel('') 3247 self._LCTRL_studies.set_string_items(items = []) 3248 self._LCTRL_series.set_string_items(items = []) 3249 self.__refresh_image() 3250 self.__refresh_details()
3251 3252 #--------------------------------------------------------
3254 self._LBL_PACS_identification.SetLabel(_('<not connected>'))
3255 3256 #--------------------------------------------------------
3257 - def __reset_ui_content(self):
3258 self.__reset_server_identification() 3259 self.__reset_patient_data() 3260 self.__set_button_states()
3261 3262 #-----------------------------------------------------
3263 - def __connect(self):
3264 3265 self.__pacs = None 3266 self.__orthanc_patient = None 3267 self.__set_button_states() 3268 self.__reset_server_identification() 3269 3270 host = self._TCTRL_host.Value.strip() 3271 port = self._TCTRL_port.Value.strip()[:6] 3272 if port == '': 3273 self._LBL_PACS_identification.SetLabel(_('Cannot connect without port (try 8042).')) 3274 return False 3275 if len(port) < 4: 3276 return False 3277 try: 3278 int(port) 3279 except ValueError: 3280 self._LBL_PACS_identification.SetLabel(_('Invalid port (try 8042).')) 3281 return False 3282 3283 user = self._TCTRL_user.Value 3284 if user == '': 3285 user = None 3286 self._LBL_PACS_identification.SetLabel(_('Connect to [%s] @ port %s as "%s".') % (host, port, user)) 3287 password = self._TCTRL_password.Value 3288 if password == '': 3289 password = None 3290 3291 pacs = gmDICOM.cOrthancServer() 3292 if not pacs.connect(host = host, port = port, user = user, password = password): #, expected_aet = 'another AET' 3293 self._LBL_PACS_identification.SetLabel(_('Cannot connect to PACS.')) 3294 _log.error('error connecting to server: %s', pacs.connect_error) 3295 return False 3296 3297 #self._LBL_PACS_identification.SetLabel(_('PACS: Orthanc "%s" (AET "%s", Version %s, API v%s, DB v%s)') % ( 3298 self._LBL_PACS_identification.SetLabel(_('PACS: Orthanc "%s" (AET "%s", Version %s, DB v%s)') % ( 3299 pacs.server_identification['Name'], 3300 pacs.server_identification['DicomAet'], 3301 pacs.server_identification['Version'], 3302 #pacs.server_identification['ApiVersion'], 3303 pacs.server_identification['DatabaseVersion'] 3304 )) 3305 3306 self.__pacs = pacs 3307 self.__set_button_states() 3308 return True
3309 3310 #--------------------------------------------------------
3311 - def __refresh_patient_data(self):
3312 3313 self.__orthanc_patient = None 3314 3315 if not self.__patient.connected: 3316 self.__reset_patient_data() 3317 self.__set_button_states() 3318 return True 3319 3320 if not self.__connect(): 3321 return False 3322 3323 tt_lines = [_('Known PACS IDs:')] 3324 for pacs_id in self.__patient.suggest_external_ids(target = 'PACS'): 3325 tt_lines.append(' ' + _('generic: %s') % pacs_id) 3326 for pacs_id in self.__patient.get_external_ids(id_type = 'PACS', issuer = self.__pacs.as_external_id_issuer): 3327 tt_lines.append(' ' + _('stored: "%(value)s" @ [%(issuer)s]') % pacs_id) 3328 tt_lines.append('') 3329 tt_lines.append(_('Patients found in PACS:')) 3330 3331 info_lines = [] 3332 # try to find patient 3333 matching_pats = self.__pacs.get_matching_patients(person = self.__patient) 3334 if len(matching_pats) == 0: 3335 info_lines.append(_('PACS: no patients with matching IDs found')) 3336 no_of_studies = 0 3337 for pat in matching_pats: 3338 info_lines.append('"%s" %s "%s (%s) %s"' % ( 3339 pat['MainDicomTags']['PatientID'], 3340 gmTools.u_arrow2right, 3341 gmTools.coalesce(pat['MainDicomTags']['PatientName'], '?'), 3342 gmTools.coalesce(pat['MainDicomTags']['PatientSex'], '?'), 3343 gmTools.coalesce(pat['MainDicomTags']['PatientBirthDate'], '?') 3344 )) 3345 no_of_studies += len(pat['Studies']) 3346 tt_lines.append('%s [#%s]' % ( 3347 gmTools.format_dict_like ( 3348 pat['MainDicomTags'], 3349 relevant_keys = ['PatientName', 'PatientSex', 'PatientBirthDate', 'PatientID'], 3350 template = ' %(PatientID)s = %(PatientName)s (%(PatientSex)s) %(PatientBirthDate)s', 3351 missing_key_template = '?' 3352 ), 3353 pat['ID'] 3354 )) 3355 if len(matching_pats) > 1: 3356 info_lines.append(_('PACS: more than one patient with matching IDs found, carefully check studies')) 3357 self._LBL_patient_identification.SetLabel('\n'.join(info_lines)) 3358 tt_lines.append('') 3359 tt_lines.append(_('Studies found: %s') % no_of_studies) 3360 self._LBL_patient_identification.SetToolTip('\n'.join(tt_lines)) 3361 3362 # get studies 3363 study_list_items = [] 3364 study_list_data = [] 3365 if len(matching_pats) > 0: 3366 # we don't at this point really expect more than one patient matching 3367 self.__orthanc_patient = matching_pats[0] 3368 for pat in self.__pacs.get_studies_list_by_orthanc_patient_list(orthanc_patients = matching_pats): 3369 for study in pat['studies']: 3370 docs = [] 3371 if study['referring_doc'] is not None: 3372 docs.append(study['referring_doc']) 3373 if study['requesting_doc'] is not None: 3374 if study['requesting_doc'] not in docs: 3375 docs.append(study['requesting_doc']) 3376 if study['performing_doc'] is not None: 3377 if study['performing_doc'] not in docs: 3378 docs.append(study['requesting_doc']) 3379 if study['operator_name'] is not None: 3380 if study['operator_name'] not in docs: 3381 docs.append(study['operator_name']) 3382 if study['radiographer_code'] is not None: 3383 if study['radiographer_code'] not in docs: 3384 docs.append(study['radiographer_code']) 3385 org_name = u'@'.join ([ 3386 o for o in [study['radiology_dept'], study['radiology_org']] 3387 if o is not None 3388 ]) 3389 org = '%s%s%s' % ( 3390 org_name, 3391 gmTools.coalesce(study['station_name'], '', ' [%s]'), 3392 gmTools.coalesce(study['radiology_org_addr'], '', ' (%s)').replace('\r\n', ' [CR] ') 3393 ) 3394 if study['date'] is None: 3395 study_date = '?' 3396 else: 3397 study_date = '%s-%s-%s' % ( 3398 study['date'][:4], 3399 study['date'][4:6], 3400 study['date'][6:8] 3401 ) 3402 study_list_items.append ( [ 3403 study_date, 3404 _('%s series%s') % ( 3405 len(study['series']), 3406 gmTools.coalesce(study['description'], '', ': %s') 3407 ), 3408 org.strip(), 3409 gmTools.u_arrow2right.join(docs) 3410 ] ) 3411 study_list_data.append(study) 3412 3413 self._LCTRL_studies.set_string_items(items = study_list_items) 3414 self._LCTRL_studies.set_data(data = study_list_data) 3415 self._LCTRL_studies.SortListItems(0, 0) 3416 self._LCTRL_studies.set_column_widths() 3417 3418 self.__refresh_image() 3419 self.__refresh_details() 3420 self.__set_button_states() 3421 3422 return True
3423 3424 #--------------------------------------------------------
3425 - def __refresh_details(self):
3426 3427 self._LCTRL_details.remove_items_safely() 3428 if self.__pacs is None: 3429 return 3430 3431 # study available ? 3432 study_data = self._LCTRL_studies.get_selected_item_data(only_one = True) 3433 if study_data is None: 3434 return 3435 items = [] 3436 items = [ [key, study_data['all_tags'][key]] for key in study_data['all_tags'] if ('%s' % study_data['all_tags'][key]).strip() != '' ] 3437 3438 # series available ? 3439 series = self._LCTRL_series.get_selected_item_data(only_one = True) 3440 if series is None: 3441 self._LCTRL_details.set_string_items(items = items) 3442 self._LCTRL_details.set_column_widths() 3443 return 3444 items.append ([ 3445 ' %s ' % (gmTools.u_box_horiz_single * 5), 3446 '%s %s %s' % ( 3447 gmTools.u_box_horiz_single * 3, 3448 _('Series'), 3449 gmTools.u_box_horiz_single * 10 3450 ) 3451 ]) 3452 items.extend([ [key, series['all_tags'][key]] for key in series['all_tags'] if ('%s' % series['all_tags'][key]).strip() != '' ]) 3453 3454 # image available ? 3455 if self.__image_data is None: 3456 self._LCTRL_details.set_string_items(items = items) 3457 self._LCTRL_details.set_column_widths() 3458 return 3459 items.append ([ 3460 ' %s ' % (gmTools.u_box_horiz_single * 5), 3461 '%s %s %s' % ( 3462 gmTools.u_box_horiz_single * 3, 3463 _('Image'), 3464 gmTools.u_box_horiz_single * 10 3465 ) 3466 ]) 3467 tags = self.__pacs.get_instance_dicom_tags(instance_id = self.__image_data['uuid']) 3468 if tags is False: 3469 items.extend(['image', '<tags not found in PACS>']) 3470 else: 3471 items.extend([ [key, tags[key]] for key in tags if ('%s' % tags[key]).strip() != '' ]) 3472 3473 self._LCTRL_details.set_string_items(items = items) 3474 self._LCTRL_details.set_column_widths()
3475 3476 #--------------------------------------------------------
3477 - def __refresh_image(self, idx=None):
3478 3479 self.__image_data = None 3480 self._SZR_image_buttons.StaticBox.SetLabel(_('Image')) 3481 self._BMP_preview.SetBitmap(wx.Bitmap.FromRGBA(50,50, red=0, green=0, blue=0, alpha = wx.ALPHA_TRANSPARENT)) 3482 3483 if idx is None: 3484 self._BMP_preview.ContainingSizer.Layout() 3485 return 3486 if self.__pacs is None: 3487 self._BMP_preview.ContainingSizer.Layout() 3488 return 3489 series = self._LCTRL_series.get_selected_item_data(only_one = True) 3490 if series is None: 3491 self._BMP_preview.ContainingSizer.Layout() 3492 return 3493 if idx > len(series['instances']) - 1: 3494 raise ValueError('trying to go beyond instances in series: %s of %s', idx, len(series['instances'])) 3495 3496 # get image 3497 uuid = series['instances'][idx] 3498 img_file = self.__pacs.get_instance_preview(instance_id = uuid) 3499 # scale 3500 wx_bmp = gmGuiHelpers.file2scaled_image(filename = img_file, height = 100) 3501 # show 3502 if wx_bmp is None: 3503 _log.error('cannot load DICOM instance from PACS: %s', uuid) 3504 else: 3505 self.__image_data = {'idx': idx, 'uuid': uuid} 3506 self._BMP_preview.SetBitmap(wx_bmp) 3507 self._SZR_image_buttons.StaticBox.SetLabel(_('Image %s/%s') % (idx+1, len(series['instances']))) 3508 3509 if idx == 0: 3510 self._BTN_previous_image.Disable() 3511 else: 3512 self._BTN_previous_image.Enable() 3513 if idx == len(series['instances']) - 1: 3514 self._BTN_next_image.Disable() 3515 else: 3516 self._BTN_next_image.Enable() 3517 3518 self._BMP_preview.ContainingSizer.Layout()
3519 3520 #--------------------------------------------------------
3521 - def __show_image(self, as_dcm=False, as_png=False):
3522 if self.__image_data is None: 3523 return False 3524 3525 uuid = self.__image_data['uuid'] 3526 img_file = None 3527 if as_dcm: 3528 img_file = self.__pacs.get_instance(instance_id = uuid) 3529 if as_png: 3530 img_file = self.__pacs.get_instance_preview(instance_id = uuid) 3531 if img_file is not None: 3532 (success, msg) = gmMimeLib.call_viewer_on_file(img_file) 3533 if not success: 3534 gmGuiHelpers.gm_show_warning ( 3535 aMessage = _('Cannot show image:\n%s') % msg, 3536 aTitle = _('Previewing DICOM image') 3537 ) 3538 return success 3539 3540 # try DCM 3541 img_file = self.__pacs.get_instance(instance_id = uuid) 3542 (success, msg) = gmMimeLib.call_viewer_on_file(img_file) 3543 if success: 3544 return True 3545 3546 # try PNG 3547 img_file = self.__pacs.get_instance_preview(instance_id = uuid) 3548 (success, msg) = gmMimeLib.call_viewer_on_file(img_file) 3549 if success: 3550 return True 3551 3552 gmGuiHelpers.gm_show_warning ( 3553 aMessage = _('Cannot show in DICOM or image viewer:\n%s') % msg, 3554 aTitle = _('Previewing DICOM image') 3555 )
3556 3557 #--------------------------------------------------------
3558 - def __save_image(self, as_dcm=False, as_png=False, nice_filename=False):
3559 if self.__image_data is None: 3560 return False, None 3561 3562 fnames = {} 3563 uuid = self.__image_data['uuid'] 3564 if as_dcm: 3565 if nice_filename: 3566 fname = gmTools.get_unique_filename ( 3567 prefix = '%s-orthanc_%s--' % (self.__patient.subdir_name, uuid), 3568 suffix = '.dcm', 3569 tmp_dir = os.path.join(gmTools.gmPaths().home_dir, 'gnumed') 3570 ) 3571 else: 3572 fname = None 3573 img_fname = self.__pacs.get_instance(filename = fname, instance_id = uuid) 3574 if img_fname is None: 3575 gmGuiHelpers.gm_show_warning ( 3576 aMessage = _('Cannot save image as DICOM file.'), 3577 aTitle = _('Saving DICOM image') 3578 ) 3579 return False, fnames 3580 3581 fnames['dcm'] = img_fname 3582 gmDispatcher.send(signal = 'statustext', msg = _('Successfully saved as [%s].') % img_fname) 3583 3584 if as_png: 3585 if nice_filename: 3586 fname = gmTools.get_unique_filename ( 3587 prefix = '%s-orthanc_%s--' % (self.__patient.subdir_name, uuid), 3588 suffix = '.png', 3589 tmp_dir = os.path.join(gmTools.gmPaths().home_dir, 'gnumed') 3590 ) 3591 else: 3592 fname = None 3593 img_fname = self.__pacs.get_instance_preview(filename = fname, instance_id = uuid) 3594 if img_fname is None: 3595 gmGuiHelpers.gm_show_warning ( 3596 aMessage = _('Cannot save image as PNG file.'), 3597 aTitle = _('Saving DICOM image') 3598 ) 3599 return False, fnames 3600 fnames['png'] = img_fname 3601 gmDispatcher.send(signal = 'statustext', msg = _('Successfully saved as [%s].') % img_fname) 3602 3603 return True, fnames
3604 3605 #--------------------------------------------------------
3607 if self.__image_data is None: 3608 return False 3609 3610 success, fnames = self.__save_image(as_dcm = True, as_png = True) 3611 if not success: 3612 return False 3613 3614 wx.BeginBusyCursor() 3615 self.__patient.export_area.add_files ( 3616 filenames = [fnames['png'], fnames['dcm']], 3617 hint = _('DICOM image of [%s] from Orthanc PACS "%s" (AET "%s")') % ( 3618 self.__orthanc_patient['MainDicomTags']['PatientID'], 3619 self.__pacs.server_identification['Name'], 3620 self.__pacs.server_identification['DicomAet'] 3621 ) 3622 ) 3623 wx.EndBusyCursor() 3624 3625 gmDispatcher.send(signal = 'statustext', msg = _('Successfully stored in export area.'))
3626 3627 #-------------------------------------------------------- 3628 #--------------------------------------------------------
3629 - def __browse_studies(self):
3630 if self.__pacs is None: 3631 return 3632 3633 study_data = self._LCTRL_studies.get_selected_item_data(only_one = True) 3634 if len(study_data) == 0: 3635 return 3636 3637 gmNetworkTools.open_url_in_browser ( 3638 self.__pacs.get_url_browse_study(study_id = study_data['orthanc_id']), 3639 new = 2, 3640 autoraise = True 3641 )
3642 3643 #--------------------------------------------------------
3644 - def __show_studies(self):
3645 if self.__pacs is None: 3646 return 3647 3648 study_data = self._LCTRL_studies.get_selected_item_data(only_one = False) 3649 if len(study_data) == 0: 3650 return 3651 3652 wx.BeginBusyCursor() 3653 target_dir = self.__pacs.get_studies_with_dicomdir(study_ids = [ s['orthanc_id'] for s in study_data ]) 3654 wx.EndBusyCursor() 3655 if target_dir is False: 3656 gmGuiHelpers.gm_show_error ( 3657 title = _('Showing DICOM studies'), 3658 error = _('Unable to show selected studies.') 3659 ) 3660 return 3661 DICOMDIR = os.path.join(target_dir, 'DICOMDIR') 3662 if os.path.isfile(DICOMDIR): 3663 (success, msg) = gmMimeLib.call_viewer_on_file(DICOMDIR, block = False) 3664 if success: 3665 return 3666 else: 3667 _log.error('cannot find DICOMDIR in: %s', target_dir) 3668 3669 gmMimeLib.call_viewer_on_file(target_dir, block = False)
3670 3671 # FIXME: on failure export as JPG and call dir viewer 3672 3673 #--------------------------------------------------------
3675 if self.__pacs is None: 3676 return 3677 3678 study_data = self._LCTRL_studies.get_item_data() 3679 if len(study_data) == 0: 3680 return 3681 3682 self.__copy_studies_to_export_area(study_data)
3683 3684 #--------------------------------------------------------
3686 if self.__pacs is None: 3687 return 3688 3689 study_data = self._LCTRL_studies.get_selected_item_data(only_one = False) 3690 if len(study_data) == 0: 3691 return 3692 3693 self.__copy_studies_to_export_area(study_data)
3694 3695 #--------------------------------------------------------
3696 - def __copy_studies_to_export_area(self, study_data):
3697 wx.BeginBusyCursor() 3698 target_dir = gmTools.mk_sandbox_dir ( 3699 prefix = 'dcm-', 3700 base_dir = os.path.join(gmTools.gmPaths().home_dir, '.gnumed', self.__patient.subdir_name) 3701 ) 3702 target_dir = self.__pacs.get_studies_with_dicomdir(study_ids = [ s['orthanc_id'] for s in study_data ], target_dir = target_dir) 3703 if target_dir is False: 3704 wx.EndBusyCursor() 3705 gmGuiHelpers.gm_show_error ( 3706 title = _('Copying DICOM studies'), 3707 error = _('Unable to put studies into export area.') 3708 ) 3709 return 3710 3711 comment = _('DICOM studies of [%s] from Orthanc PACS "%s" (AET "%s") [%s/]') % ( 3712 self.__orthanc_patient['MainDicomTags']['PatientID'], 3713 self.__pacs.server_identification['Name'], 3714 self.__pacs.server_identification['DicomAet'], 3715 target_dir 3716 ) 3717 if self.__patient.export_area.add_path(target_dir, comment): 3718 wx.EndBusyCursor() 3719 return 3720 3721 wx.EndBusyCursor() 3722 gmGuiHelpers.gm_show_error ( 3723 title = _('Adding DICOM studies to export area'), 3724 error = _('Cannot add the following path to the export area:\n%s ') % target_dir 3725 )
3726 3727 #--------------------------------------------------------
3729 if self.__pacs is None: 3730 return 3731 3732 study_data = self._LCTRL_studies.get_item_data() 3733 if len(study_data) == 0: 3734 return 3735 3736 self.__copy_zip_of_studies_to_export_area(study_data)
3737 3738 #--------------------------------------------------------
3740 if self.__pacs is None: 3741 return 3742 3743 study_data = self._LCTRL_studies.get_selected_item_data(only_one = False) 3744 if len(study_data) == 0: 3745 return 3746 3747 self.__copy_zip_of_studies_to_export_area(study_data)
3748 3749 #--------------------------------------------------------
3750 - def __copy_zip_of_studies_to_export_area(self, study_data):
3751 wx.BeginBusyCursor() 3752 zip_fname = self.__pacs.get_studies_with_dicomdir ( 3753 study_ids = [ s['orthanc_id'] for s in study_data ], 3754 create_zip = True 3755 ) 3756 if zip_fname is False: 3757 wx.EndBusyCursor() 3758 gmGuiHelpers.gm_show_error ( 3759 title = _('Adding DICOM studies to export area'), 3760 error = _('Unable to put ZIP of studies into export area.') 3761 ) 3762 return 3763 3764 # check size and confirm if huge 3765 zip_size = os.path.getsize(zip_fname) 3766 if zip_size > (300 * gmTools._MB): # ~ 1/2 CD-ROM 3767 wx.EndBusyCursor() 3768 really_export = gmGuiHelpers.gm_show_question ( 3769 title = _('Exporting DICOM studies'), 3770 question = _('The DICOM studies are %s in compressed size.\n\nReally move into export area ?') % gmTools.size2str(zip_size), 3771 cancel_button = False 3772 ) 3773 if not really_export: 3774 return 3775 3776 hint = _('DICOM studies of [%s] from Orthanc PACS "%s" (AET "%s")') % ( 3777 self.__orthanc_patient['MainDicomTags']['PatientID'], 3778 self.__pacs.server_identification['Name'], 3779 self.__pacs.server_identification['DicomAet'] 3780 ) 3781 if self.__patient.export_area.add_file(filename = zip_fname, hint = hint): 3782 #gmDispatcher.send(signal = 'statustext', msg = _('Successfully saved as [%s].') % filename) 3783 wx.EndBusyCursor() 3784 return 3785 3786 wx.EndBusyCursor() 3787 gmGuiHelpers.gm_show_error ( 3788 title = _('Adding DICOM studies to export area'), 3789 error = _('Cannot add the following archive to the export area:\n%s ') % zip_fname 3790 )
3791 3792 #--------------------------------------------------------
3793 - def __save_selected_studies(self):
3794 if self.__pacs is None: 3795 return 3796 3797 study_data = self._LCTRL_studies.get_selected_item_data(only_one = False) 3798 if len(study_data) == 0: 3799 return 3800 3801 self.__save_studies_to_disk(study_data)
3802 3803 #--------------------------------------------------------
3804 - def __on_save_all_studies(self):
3805 if self.__pacs is None: 3806 return 3807 3808 study_data = self._LCTRL_studies.get_item_data() 3809 if len(study_data) == 0: 3810 return 3811 3812 self.__save_studies_to_disk(study_data)
3813 3814 #--------------------------------------------------------
3815 - def __save_studies_to_disk(self, study_data):
3816 default_path = os.path.join(gmTools.gmPaths().home_dir, 'gnumed', self.__patient.subdir_name) 3817 gmTools.mkdir(default_path) 3818 dlg = wx.DirDialog ( 3819 self, 3820 message = _('Select the directory into which to save the DICOM studies.'), 3821 defaultPath = default_path 3822 ) 3823 choice = dlg.ShowModal() 3824 target_dir = dlg.GetPath() 3825 dlg.DestroyLater() 3826 if choice != wx.ID_OK: 3827 return True 3828 3829 wx.BeginBusyCursor() 3830 target_dir = self.__pacs.get_studies_with_dicomdir(study_ids = [ s['orthanc_id'] for s in study_data ], target_dir = target_dir) 3831 wx.EndBusyCursor() 3832 3833 if target_dir is False: 3834 gmGuiHelpers.gm_show_error ( 3835 title = _('Saving DICOM studies'), 3836 error = _('Unable to save DICOM studies.') 3837 ) 3838 return 3839 gmDispatcher.send(signal = 'statustext', msg = _('Successfully saved to [%s].') % target_dir)
3840 3841 #--------------------------------------------------------
3843 if self.__pacs is None: 3844 return 3845 3846 study_data = self._LCTRL_studies.get_selected_item_data(only_one = False) 3847 if len(study_data) == 0: 3848 return 3849 3850 self.__save_zip_of_studies_to_disk(study_data)
3851 3852 #--------------------------------------------------------
3854 if self.__pacs is None: 3855 return 3856 3857 study_data = self._LCTRL_studies.get_item_data() 3858 if len(study_data) == 0: 3859 return 3860 3861 self.__save_zip_of_studies_to_disk(study_data)
3862 3863 #--------------------------------------------------------
3864 - def __save_zip_of_studies_to_disk(self, study_data):
3865 default_path = os.path.join(gmTools.gmPaths().home_dir, 'gnumed', self.__patient.subdir_name) 3866 gmTools.mkdir(default_path) 3867 dlg = wx.DirDialog ( 3868 self, 3869 message = _('Select the directory into which to save the DICOM studies ZIP.'), 3870 defaultPath = default_path 3871 ) 3872 choice = dlg.ShowModal() 3873 target_dir = dlg.GetPath() 3874 dlg.DestroyLater() 3875 if choice != wx.ID_OK: 3876 return True 3877 3878 wx.BeginBusyCursor() 3879 filename = self.__pacs.get_studies_with_dicomdir(study_ids = [ s['orthanc_id'] for s in study_data ], target_dir = target_dir, create_zip = True) 3880 wx.EndBusyCursor() 3881 3882 if filename is False: 3883 gmGuiHelpers.gm_show_error ( 3884 title = _('Saving DICOM studies'), 3885 error = _('Unable to save DICOM studies as ZIP.') 3886 ) 3887 return 3888 3889 gmDispatcher.send(signal = 'statustext', msg = _('Successfully saved as [%s].') % filename)
3890 3891 #--------------------------------------------------------
3892 - def _on_add_pdf_to_study(self, evt):
3893 if self.__pacs is None: 3894 return 3895 3896 study_data = self._LCTRL_studies.get_selected_item_data(only_one = False) 3897 if len(study_data) != 1: 3898 gmGuiHelpers.gm_show_info ( 3899 title = _('Adding PDF to DICOM study'), 3900 info = _('For adding a PDF file there must be exactly one (1) DICOM study selected.') 3901 ) 3902 return 3903 3904 # select PDF 3905 pdf_name = None 3906 dlg = wx.FileDialog ( 3907 parent = self, 3908 message = _('Select PDF to add to DICOM study'), 3909 defaultDir = os.path.join(gmTools.gmPaths().home_dir, 'gnumed'), 3910 wildcard = "%s (*.pdf)|*.pdf|%s (*)|*" % (_('PDF files'), _('all files')), 3911 style = wx.FD_OPEN | wx.FD_FILE_MUST_EXIST 3912 ) 3913 choice = dlg.ShowModal() 3914 pdf_name = dlg.GetPath() 3915 dlg.DestroyLater() 3916 if choice != wx.ID_OK: 3917 return 3918 3919 _log.debug('dicomize(%s)', pdf_name) 3920 if pdf_name is None: 3921 return 3922 3923 # export one instance as template 3924 instance_uuid = study_data[0]['series'][0]['instances'][-1] 3925 dcm_instance_template_fname = self.__pacs.get_instance(instance_id = instance_uuid) 3926 # dicomize PDF via template 3927 _cfg = gmCfg2.gmCfgData() 3928 pdf2dcm_fname = gmDICOM.dicomize_pdf ( 3929 pdf_name = pdf_name, 3930 dcm_template_file = dcm_instance_template_fname, 3931 title = 'GNUmed', 3932 verbose = _cfg.get(option = 'debug') 3933 ) 3934 if pdf2dcm_fname is None: 3935 gmGuiHelpers.gm_show_error ( 3936 title = _('Adding PDF to DICOM study'), 3937 error = _('Cannot turn PDF file\n\n %s\n\n into DICOM file.') 3938 ) 3939 return 3940 3941 # upload pdf.dcm 3942 if self.__pacs.upload_dicom_file(pdf2dcm_fname): 3943 gmDispatcher.send(signal = 'statustext', msg = _('Successfully uploaded [%s] to Orthanc DICOM server.') % pdf2dcm_fname) 3944 return 3945 3946 gmGuiHelpers.gm_show_error ( 3947 title = _('Adding PDF to DICOM study'), 3948 error = _('Cannot updload DICOM file\n\n %s\n\n into Orthanc PACS.') % pdf2dcm_fname 3949 ) 3950 self._schedule_data_reget()
3951 3952 #-------------------------------------------------------- 3953 #--------------------------------------------------------
3954 - def __browse_patient(self):
3955 if self.__pacs is None: 3956 return 3957 3958 gmNetworkTools.open_url_in_browser ( 3959 self.__pacs.get_url_browse_patient(patient_id = self.__orthanc_patient['ID']), 3960 new = 2, 3961 autoraise = True 3962 )
3963 3964 #-------------------------------------------------------- 3965 #--------------------------------------------------------
3966 - def __browse_pacs(self):
3967 if self.__pacs is None: 3968 return 3969 3970 gmNetworkTools.open_url_in_browser ( 3971 self.__pacs.url_browse_patients, 3972 new = 2, 3973 autoraise = True 3974 )
3975 3976 #-------------------------------------------------------- 3977 # reget-on-paint mixin API 3978 #--------------------------------------------------------
3979 - def _populate_with_data(self):
3980 if not self.__patient.connected: 3981 self.__reset_ui_content() 3982 return True 3983 3984 if not self.__refresh_patient_data(): 3985 return False 3986 3987 return True
3988 3989 #-------------------------------------------------------- 3990 # event handling 3991 #--------------------------------------------------------
3992 - def __register_interests(self):
3993 3994 # wxPython signals 3995 self._BMP_preview.Bind(wx.EVT_LEFT_DCLICK, self._on_preview_image_leftdoubleclicked) 3996 self._BMP_preview.Bind(wx.EVT_RIGHT_UP, self._on_preview_image_rightclicked) 3997 self._BTN_browse_study.Bind(wx.EVT_RIGHT_UP, self._on_studies_button_rightclicked) 3998 3999 # client internal signals 4000 gmDispatcher.connect(signal = 'pre_patient_unselection', receiver = self._on_pre_patient_unselection) 4001 gmDispatcher.connect(signal = 'post_patient_selection', receiver = self._on_post_patient_selection) 4002 4003 # generic database change signal 4004 gmDispatcher.connect(signal = 'gm_table_mod', receiver = self._on_database_signal)
4005 4006 #--------------------------------------------------------
4008 # only empty out here, do NOT access the patient 4009 # or else we will access the old patient while it 4010 # may not be valid anymore ... 4011 self.__reset_patient_data()
4012 4013 #--------------------------------------------------------
4014 - def _on_post_patient_selection(self):
4015 self._schedule_data_reget()
4016 4017 #--------------------------------------------------------
4018 - def _on_database_signal(self, **kwds):
4019 4020 if not self.__patient.connected: 4021 # probably not needed: 4022 #self._schedule_data_reget() 4023 return True 4024 4025 if kwds['pk_identity'] != self.__patient.ID: 4026 return True 4027 4028 if kwds['table'] == 'dem.lnk_identity2ext_id': 4029 self._schedule_data_reget() 4030 return True 4031 4032 return True
4033 4034 #-------------------------------------------------------- 4035 # events: lists 4036 #--------------------------------------------------------
4037 - def _on_series_list_item_selected(self, event):
4038 4039 event.Skip() 4040 if self.__pacs is None: 4041 return 4042 4043 study_data = self._LCTRL_studies.get_selected_item_data(only_one = True) 4044 if study_data is None: 4045 return 4046 4047 series = self._LCTRL_series.get_selected_item_data(only_one = True) 4048 if series is None: 4049 self.__set_button_states() 4050 return 4051 4052 if len(series['instances']) == 0: 4053 self.__refresh_image() 4054 self.__refresh_details() 4055 self.__set_button_states() 4056 return 4057 4058 # set first image 4059 self.__refresh_image(0) 4060 self.__refresh_details() 4061 self.__set_button_states() 4062 self._BTN_previous_image.Disable()
4063 4064 #--------------------------------------------------------
4065 - def _on_series_list_item_deselected(self, event):
4066 event.Skip() 4067 4068 self.__refresh_image() 4069 self.__refresh_details() 4070 self.__set_button_states()
4071 4072 #--------------------------------------------------------
4073 - def _on_studies_list_item_selected(self, event):
4074 event.Skip() 4075 if self.__pacs is None: 4076 return 4077 4078 study_data = self._LCTRL_studies.get_item_data(item_idx = event.Index) 4079 series_list_items = [] 4080 series_list_data = [] 4081 for series in study_data['series']: 4082 4083 series_time = '' 4084 if series['time'] is None: 4085 series['time'] = study_data['time'] 4086 if series['time'] is None: 4087 series_time = '?' 4088 else: 4089 series_time = '%s:%s:%s' % ( 4090 series['time'][:2], 4091 series['time'][2:4], 4092 series['time'][4:6] 4093 ) 4094 4095 series_desc_parts = [] 4096 if series['description'] is not None: 4097 if series['protocol'] is None: 4098 series_desc_parts.append(series['description'].strip()) 4099 else: 4100 if series['description'].strip() not in series['protocol'].strip(): 4101 series_desc_parts.append(series['description'].strip()) 4102 if series['protocol'] is not None: 4103 series_desc_parts.append('[%s]' % series['protocol'].strip()) 4104 if series['performed_procedure_step_description'] is not None: 4105 series_desc_parts.append(series['performed_procedure_step_description'].strip()) 4106 if series['acquisition_device_processing_description'] is not None: 4107 series_desc_parts.append(series['acquisition_device_processing_description'].strip()) 4108 series_desc = ' / '.join(series_desc_parts) 4109 if len(series_desc) > 0: 4110 series_desc = ': ' + series_desc 4111 series_desc = _('%s image(s)%s') % (len(series['instances']), series_desc) 4112 4113 series_list_items.append ([ 4114 series_time, 4115 gmTools.coalesce(series['modality'], ''), 4116 gmTools.coalesce(series['body_part'], ''), 4117 series_desc 4118 ]) 4119 series_list_data.append(series) 4120 4121 self._LCTRL_series.set_string_items(items = series_list_items) 4122 self._LCTRL_series.set_data(data = series_list_data) 4123 self._LCTRL_series.SortListItems(0) 4124 4125 self.__refresh_image() 4126 self.__refresh_details() 4127 self.__set_button_states()
4128 4129 #--------------------------------------------------------
4130 - def _on_studies_list_item_deselected(self, event):
4131 event.Skip() 4132 4133 self._LCTRL_series.remove_items_safely() 4134 self.__refresh_image() 4135 self.__refresh_details() 4136 self.__set_button_states()
4137 4138 #-------------------------------------------------------- 4139 # events: buttons 4140 #--------------------------------------------------------
4141 - def _on_connect_button_pressed(self, event):
4142 event.Skip() 4143 4144 if not self.__connect(): 4145 self.__reset_patient_data() 4146 self.__set_button_states() 4147 return False 4148 4149 if not self.__refresh_patient_data(): 4150 self.__set_button_states() 4151 return False 4152 4153 self.__set_button_states() 4154 return True
4155 4156 #--------------------------------------------------------
4157 - def _on_upload_button_pressed(self, event):
4158 event.Skip() 4159 if self.__pacs is None: 4160 return 4161 4162 dlg = wx.DirDialog ( 4163 self, 4164 message = _('Select the directory from which to recursively upload DICOM files.'), 4165 defaultPath = os.path.join(gmTools.gmPaths().home_dir, 'gnumed') 4166 ) 4167 choice = dlg.ShowModal() 4168 dicom_dir = dlg.GetPath() 4169 dlg.DestroyLater() 4170 if choice != wx.ID_OK: 4171 return True 4172 wx.BeginBusyCursor() 4173 try: 4174 uploaded, not_uploaded = self.__pacs.upload_from_directory ( 4175 directory = dicom_dir, 4176 recursive = True, 4177 check_mime_type = False, 4178 ignore_other_files = True 4179 ) 4180 finally: 4181 wx.EndBusyCursor() 4182 if len(not_uploaded) == 0: 4183 q = _('Delete the uploaded DICOM files now ?') 4184 else: 4185 q = _('Some files have not been uploaded.\n\nDo you want to delete those DICOM files which have been sent to the PACS successfully ?') 4186 _log.error('not uploaded:') 4187 for f in not_uploaded: 4188 _log.error(f) 4189 4190 delete_uploaded = gmGuiHelpers.gm_show_question ( 4191 title = _('Uploading DICOM files'), 4192 question = q, 4193 cancel_button = False 4194 ) 4195 if not delete_uploaded: 4196 return 4197 wx.BeginBusyCursor() 4198 for f in uploaded: 4199 gmTools.remove_file(f) 4200 wx.EndBusyCursor()
4201 4202 #--------------------------------------------------------
4204 event.Skip() 4205 if self.__pacs is None: 4206 return 4207 4208 title = _('Working on: Orthanc "%s" (AET "%s" @ %s:%s, Version %s)') % ( 4209 self.__pacs.server_identification['Name'], 4210 self.__pacs.server_identification['DicomAet'], 4211 self._TCTRL_host.Value.strip(), 4212 self._TCTRL_port.Value.strip(), 4213 self.__pacs.server_identification['Version'] 4214 ) 4215 dlg = cModifyOrthancContentDlg(self, -1, server = self.__pacs, title = title) 4216 dlg.ShowModal() 4217 dlg.DestroyLater() 4218 self._schedule_data_reget()
4219 4220 #-------------------------------------------------------- 4221 # - image menu and image buttons 4222 #--------------------------------------------------------
4223 - def _on_show_image_as_dcm(self, event):
4224 self.__show_image(as_dcm = True)
4225 4226 #--------------------------------------------------------
4227 - def _on_show_image_as_png(self, event):
4228 self.__show_image(as_png = True)
4229 4230 #--------------------------------------------------------
4231 - def _on_copy_image_to_export_area(self, event):
4232 self.__copy_image_to_export_area()
4233 4234 #--------------------------------------------------------
4235 - def _on_save_image_as_png(self, event):
4236 self.__save_image(as_png = True, nice_filename = True)
4237 4238 #--------------------------------------------------------
4239 - def _on_save_image_as_dcm(self, event):
4240 self.__save_image(as_dcm = True, nice_filename = True)
4241 4242 #-------------------------------------------------------- 4243 #--------------------------------------------------------
4244 - def _on_preview_image_leftdoubleclicked(self, event):
4245 self.__show_image()
4246 4247 #--------------------------------------------------------
4248 - def _on_preview_image_rightclicked(self, event):
4249 if self.__image_data is None: 4250 return False 4251 4252 self.PopupMenu(self.__thumbnail_menu)
4253 4254 #--------------------------------------------------------
4255 - def _on_next_image_button_pressed(self, event):
4256 if self.__image_data is None: 4257 return 4258 4259 self.__refresh_image(idx = self.__image_data['idx'] + 1) 4260 self.__refresh_details()
4261 4262 #--------------------------------------------------------
4263 - def _on_previous_image_button_pressed(self, event):
4264 if self.__image_data is None: 4265 return 4266 self.__refresh_image(idx = self.__image_data['idx'] - 1) 4267 self.__refresh_details()
4268 4269 #--------------------------------------------------------
4270 - def _on_button_image_show_pressed(self, event):
4271 self.__show_image()
4272 4273 #--------------------------------------------------------
4274 - def _on_button_image_export_pressed(self, event):
4275 self.__copy_image_to_export_area()
4276 4277 #-------------------------------------------------------- 4278 # - study menu and buttons 4279 #--------------------------------------------------------
4280 - def _on_browse_study_button_pressed(self, event):
4281 self.__browse_studies()
4282 4283 #--------------------------------------------------------
4284 - def _on_studies_show_button_pressed(self, event):
4285 self.__show_studies()
4286 4287 #--------------------------------------------------------
4288 - def _on_studies_export_button_pressed(self, event):
4289 self.__copy_selected_studies_to_export_area()
4290 4291 #--------------------------------------------------------
4292 - def _on_studies_button_rightclicked(self, event):
4293 self.PopupMenu(self.__studies_menu)
4294 4295 #--------------------------------------------------------
4297 self.__copy_selected_studies_to_export_area()
4298 4299 #--------------------------------------------------------
4300 - def _on_copy_all_studies_to_export_area(self, event):
4301 self.__copy_all_studies_to_export_area()
4302 4303 #--------------------------------------------------------
4305 self.__copy_zip_of_selected_studies_to_export_area()
4306 4307 #--------------------------------------------------------
4309 self.__copy_zip_of_all_studies_to_export_area()
4310 4311 #--------------------------------------------------------
4312 - def _on_save_selected_studies(self, event):
4313 self.__save_selected_studies()
4314 4315 #--------------------------------------------------------
4316 - def _on_save_zip_of_selected_studies(self, event):
4317 self.__save_zip_of_selected_studies()
4318 4319 #--------------------------------------------------------
4320 - def _on_save_all_studies(self, event):
4321 self.__save_all_studies()
4322 4323 #--------------------------------------------------------
4324 - def _on_save_zip_of_all_studies(self, event):
4325 self.__save_zip_of_all_studies()
4326 4327 #-------------------------------------------------------- 4328 # - patient buttons (= all studies) 4329 #--------------------------------------------------------
4330 - def _on_browse_patient_button_pressed(self, event):
4331 self.__browse_patient()
4332 4333 #--------------------------------------------------------
4335 if self.__pacs is None: 4336 return None 4337 4338 if self.__orthanc_patient is None: 4339 return None 4340 4341 patient_id = self.__orthanc_patient['ID'] 4342 wx.BeginBusyCursor() 4343 try: 4344 bad_data = self.__pacs.verify_patient_data(patient_id) 4345 finally: 4346 wx.EndBusyCursor() 4347 if len(bad_data) == 0: 4348 gmDispatcher.send(signal = 'statustext', msg = _('Successfully verified DICOM data of patient.')) 4349 return 4350 4351 gmGuiHelpers.gm_show_error ( 4352 title = _('DICOM data error'), 4353 error = _( 4354 'There seems to be a data error in the DICOM files\n' 4355 'stored in the Orthanc server.\n' 4356 '\n' 4357 'Please check the inbox.' 4358 ) 4359 ) 4360 4361 msg = _('Checksum error in DICOM data of this patient.\n\n') 4362 msg += _('DICOM server: %s\n\n') % bad_data[0]['orthanc'] 4363 for bd in bad_data: 4364 msg += _('Orthanc patient ID [%s]\n %s: [%s]\n') % ( 4365 bd['patient'], 4366 bd['type'], 4367 bd['instance'] 4368 ) 4369 prov = self.__patient.primary_provider 4370 if prov is None: 4371 prov = gmStaff.gmCurrentProvider() 4372 report = gmProviderInbox.create_inbox_message ( 4373 message_type = _('error report'), 4374 message_category = 'clinical', 4375 patient = self.__patient.ID, 4376 staff = prov['pk_staff'], 4377 subject = _('DICOM data corruption') 4378 ) 4379 report['data'] = msg 4380 report.save()
4381 4382 #--------------------------------------------------------
4383 - def _on_browse_pacs_button_pressed(self, event):
4384 self.__browse_pacs()
4385 4386 #------------------------------------------------------------ 4387 from Gnumed.wxGladeWidgets.wxgModifyOrthancContentDlg import wxgModifyOrthancContentDlg 4388
4389 -class cModifyOrthancContentDlg(wxgModifyOrthancContentDlg):
4390 - def __init__(self, *args, **kwds):
4391 self.__srv = kwds['server'] 4392 del kwds['server'] 4393 title = kwds['title'] 4394 del kwds['title'] 4395 wxgModifyOrthancContentDlg.__init__(self, *args, **kwds) 4396 self.SetTitle(title) 4397 self._LCTRL_patients.set_columns( [_('Patient ID'), _('Name'), _('Birth date'), _('Gender'), _('Orthanc')] )
4398 4399 #--------------------------------------------------------
4400 - def __refresh_patient_list(self):
4401 self._LCTRL_patients.set_string_items() 4402 search_term = self._TCTRL_search_term.Value.strip() 4403 if search_term == '': 4404 return 4405 pats = self.__srv.get_patients_by_name(name_parts = search_term.split(), fuzzy = True) 4406 if len(pats) == 0: 4407 return 4408 list_items = [] 4409 list_data = [] 4410 for pat in pats: 4411 mt = pat['MainDicomTags'] 4412 try: 4413 gender = mt['PatientSex'] 4414 except KeyError: 4415 gender = '' 4416 try: 4417 dob = mt['PatientBirthDate'] 4418 except KeyError: 4419 dob = '' 4420 list_items.append([mt['PatientID'], mt['PatientName'], dob, gender, pat['ID']]) 4421 list_data.append(mt['PatientID']) 4422 self._LCTRL_patients.set_string_items(list_items) 4423 self._LCTRL_patients.set_column_widths() 4424 self._LCTRL_patients.set_data(list_data)
4425 4426 #--------------------------------------------------------
4427 - def _on_search_patients_button_pressed(self, event):
4428 event.Skip() 4429 self.__refresh_patient_list()
4430 4431 #--------------------------------------------------------
4433 event.Skip() 4434 pat = gmPerson.gmCurrentPatient() 4435 if not pat.connected: 4436 return 4437 self._TCTRL_new_patient_id.Value = pat.suggest_external_id(target = 'PACS')
4438 4439 #--------------------------------------------------------
4440 - def _on_set_patient_id_button_pressed(self, event):
4441 event.Skip() 4442 new_id = self._TCTRL_new_patient_id.Value.strip() 4443 if new_id == '': 4444 return 4445 pats = self._LCTRL_patients.get_selected_item_data(only_one = False) 4446 if len(pats) == 0: 4447 return 4448 really_modify = gmGuiHelpers.gm_show_question ( 4449 title = _('Modifying patient ID'), 4450 question = _( 4451 'Really modify %s patient(s) to have the new patient ID\n\n' 4452 ' [%s]\n\n' 4453 'stored in the Orthanc DICOM server ?' 4454 ) % ( 4455 len(pats), 4456 new_id 4457 ), 4458 cancel_button = True 4459 ) 4460 if not really_modify: 4461 return 4462 all_modified = True 4463 for pat in pats: 4464 success = self.__srv.modify_patient_id(old_patient_id = pat, new_patient_id = new_id) 4465 if not success: 4466 all_modified = False 4467 self.__refresh_patient_list() 4468 4469 if not all_modified: 4470 gmGuiHelpers.gm_show_warning ( 4471 aTitle = _('Modifying patient ID'), 4472 aMessage = _( 4473 'I was unable to modify all DICOM patients.\n' 4474 '\n' 4475 'Please refer to the log file.' 4476 ) 4477 ) 4478 return all_modified
4479 4480 #------------------------------------------------------------ 4481 # outdated:
4482 -def upload_files():
4483 event.Skip() 4484 dlg = wx.DirDialog ( 4485 self, 4486 message = _('Select the directory from which to recursively upload DICOM files.'), 4487 defaultPath = os.path.join(gmTools.gmPaths().home_dir, 'gnumed') 4488 ) 4489 choice = dlg.ShowModal() 4490 dicom_dir = dlg.GetPath() 4491 dlg.DestroyLater() 4492 if choice != wx.ID_OK: 4493 return True 4494 wx.BeginBusyCursor() 4495 try: 4496 uploaded, not_uploaded = self.__pacs.upload_from_directory ( 4497 directory = dicom_dir, 4498 recursive = True, 4499 check_mime_type = False, 4500 ignore_other_files = True 4501 ) 4502 finally: 4503 wx.EndBusyCursor() 4504 if len(not_uploaded) == 0: 4505 q = _('Delete the uploaded DICOM files now ?') 4506 else: 4507 q = _('Some files have not been uploaded.\n\nDo you want to delete those DICOM files which have been sent to the PACS successfully ?') 4508 _log.error('not uploaded:') 4509 for f in not_uploaded: 4510 _log.error(f) 4511 delete_uploaded = gmGuiHelpers.gm_show_question ( 4512 title = _('Uploading DICOM files'), 4513 question = q, 4514 cancel_button = False 4515 ) 4516 if not delete_uploaded: 4517 return 4518 wx.BeginBusyCursor() 4519 for f in uploaded: 4520 gmTools.remove_file(f) 4521 wx.EndBusyCursor()
4522 4523 #============================================================ 4524 # main 4525 #------------------------------------------------------------ 4526 if __name__ == '__main__': 4527 4528 if len(sys.argv) < 2: 4529 sys.exit() 4530 4531 if sys.argv[1] != 'test': 4532 sys.exit() 4533 4534 from Gnumed.business import gmPersonSearch 4535 from Gnumed.wxpython import gmPatSearchWidgets 4536 4537 #----------------------------------------------------------------
4538 - def test_document_prw():
4539 app = wx.PyWidgetTester(size = (180, 20)) 4540 #pnl = cEncounterEditAreaPnl(app.frame, -1, encounter=enc) 4541 prw = cDocumentPhraseWheel(app.frame, -1) 4542 prw.set_context('pat', 12) 4543 app.frame.Show(True) 4544 app.MainLoop()
4545 4546 #---------------------------------------------------------------- 4547 test_document_prw() 4548