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