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