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