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