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

Source Code for Module Gnumed.wxpython.gmDocumentWidgets

   1  """GNUmed medical document handling widgets. 
   2  """ 
   3  #================================================================ 
   4  __version__ = "$Revision: 1.187 $" 
   5  __author__ = "Karsten Hilbert <Karsten.Hilbert@gmx.net>" 
   6   
   7  import os.path 
   8  import sys 
   9  import re as regex 
  10  import logging 
  11   
  12   
  13  import wx 
  14   
  15   
  16  if __name__ == '__main__': 
  17          sys.path.insert(0, '../../') 
  18  from Gnumed.pycommon import gmI18N, gmCfg, gmPG2, gmMimeLib, gmExceptions, gmMatchProvider, gmDispatcher, gmDateTime, gmTools, gmShellAPI, gmHooks 
  19  from Gnumed.business import gmPerson 
  20  from Gnumed.business import gmDocuments 
  21  from Gnumed.business import gmEMRStructItems 
  22  from Gnumed.business import gmSurgery 
  23   
  24  from Gnumed.wxpython import gmGuiHelpers 
  25  from Gnumed.wxpython import gmRegetMixin 
  26  from Gnumed.wxpython import gmPhraseWheel 
  27  from Gnumed.wxpython import gmPlugin 
  28  from Gnumed.wxpython import gmEMRStructWidgets 
  29  from Gnumed.wxpython import gmListWidgets 
  30   
  31   
  32  _log = logging.getLogger('gm.ui') 
  33  _log.info(__version__) 
  34   
  35   
  36  default_chunksize = 1 * 1024 * 1024             # 1 MB 
  37  #============================================================ 
38 -def manage_document_descriptions(parent=None, document=None):
39 40 #----------------------------------- 41 def delete_item(item): 42 doit = gmGuiHelpers.gm_show_question ( 43 _( 'Are you sure you want to delete this\n' 44 'description from the document ?\n' 45 ), 46 _('Deleting document description') 47 ) 48 if not doit: 49 return True 50 51 document.delete_description(pk = item[0]) 52 return True
53 #----------------------------------- 54 def add_item(): 55 dlg = gmGuiHelpers.cMultilineTextEntryDlg ( 56 parent, 57 -1, 58 title = _('Adding document description'), 59 msg = _('Below you can add a document description.\n') 60 ) 61 result = dlg.ShowModal() 62 if result == wx.ID_SAVE: 63 document.add_description(dlg.value) 64 65 dlg.Destroy() 66 return True 67 #----------------------------------- 68 def edit_item(item): 69 dlg = gmGuiHelpers.cMultilineTextEntryDlg ( 70 parent, 71 -1, 72 title = _('Editing document description'), 73 msg = _('Below you can edit the document description.\n'), 74 text = item[1] 75 ) 76 result = dlg.ShowModal() 77 if result == wx.ID_SAVE: 78 document.update_description(pk = item[0], description = dlg.value) 79 80 dlg.Destroy() 81 return True 82 #----------------------------------- 83 def refresh_list(lctrl): 84 descriptions = document.get_descriptions() 85 86 lctrl.set_string_items(items = [ 87 u'%s%s' % ( (u' '.join(regex.split('\r\n+|\r+|\n+|\t+', desc[1])))[:30], gmTools.u_ellipsis ) 88 for desc in descriptions 89 ]) 90 lctrl.set_data(data = descriptions) 91 #----------------------------------- 92 93 gmListWidgets.get_choices_from_list ( 94 parent = parent, 95 msg = _('Select the description you are interested in.\n'), 96 caption = _('Managing document descriptions'), 97 columns = [_('Description')], 98 edit_callback = edit_item, 99 new_callback = add_item, 100 delete_callback = delete_item, 101 refresh_callback = refresh_list, 102 single_selection = True, 103 can_return_empty = True 104 ) 105 106 return True 107 #============================================================
108 -def _save_file_as_new_document(**kwargs):
109 try: 110 del kwargs['signal'] 111 del kwargs['sender'] 112 except KeyError: 113 pass 114 wx.CallAfter(save_file_as_new_document, **kwargs)
115
116 -def _save_files_as_new_document(**kwargs):
117 try: 118 del kwargs['signal'] 119 del kwargs['sender'] 120 except KeyError: 121 pass 122 wx.CallAfter(save_files_as_new_document, **kwargs)
123 #----------------------
124 -def save_file_as_new_document(parent=None, filename=None, document_type=None, unlock_patient=False, episode=None, review_as_normal=False):
125 return save_files_as_new_document ( 126 parent = parent, 127 filenames = [filename], 128 document_type = document_type, 129 unlock_patient = unlock_patient, 130 episode = episode, 131 review_as_normal = review_as_normal 132 )
133 #----------------------
134 -def save_files_as_new_document(parent=None, filenames=None, document_type=None, unlock_patient=False, episode=None, review_as_normal=False):
135 136 pat = gmPerson.gmCurrentPatient() 137 if not pat.connected: 138 return None 139 140 emr = pat.get_emr() 141 142 if parent is None: 143 parent = wx.GetApp().GetTopWindow() 144 145 if episode is None: 146 all_epis = emr.get_episodes() 147 # FIXME: what to do here ? probably create dummy episode 148 if len(all_epis) == 0: 149 episode = emr.add_episode(episode_name = _('Documents'), is_open = False) 150 else: 151 dlg = gmEMRStructWidgets.cEpisodeListSelectorDlg(parent = parent, id = -1, episodes = all_epis) 152 dlg.SetTitle(_('Select the episode under which to file the document ...')) 153 btn_pressed = dlg.ShowModal() 154 episode = dlg.get_selected_item_data(only_one = True) 155 dlg.Destroy() 156 157 if (btn_pressed == wx.ID_CANCEL) or (episode is None): 158 if unlock_patient: 159 pat.locked = False 160 return None 161 162 doc_type = gmDocuments.create_document_type(document_type = document_type) 163 164 docs_folder = pat.get_document_folder() 165 doc = docs_folder.add_document ( 166 document_type = doc_type['pk_doc_type'], 167 encounter = emr.active_encounter['pk_encounter'], 168 episode = episode['pk_episode'] 169 ) 170 doc.add_parts_from_files(files = filenames) 171 172 if review_as_normal: 173 doc.set_reviewed(technically_abnormal = False, clinically_relevant = False) 174 175 if unlock_patient: 176 pat.locked = False 177 178 gmDispatcher.send(signal = 'statustext', msg = _('Imported new document from %s.') % filenames, beep = True) 179 180 return doc
181 #---------------------- 182 gmDispatcher.connect(signal = u'import_document_from_file', receiver = _save_file_as_new_document) 183 gmDispatcher.connect(signal = u'import_document_from_files', receiver = _save_files_as_new_document) 184 #============================================================
185 -class cDocumentCommentPhraseWheel(gmPhraseWheel.cPhraseWheel):
186 """Let user select a document comment from all existing comments."""
187 - def __init__(self, *args, **kwargs):
188 189 gmPhraseWheel.cPhraseWheel.__init__(self, *args, **kwargs) 190 191 context = { 192 u'ctxt_doc_type': { 193 u'where_part': u'and fk_type = %(pk_doc_type)s', 194 u'placeholder': u'pk_doc_type' 195 } 196 } 197 198 mp = gmMatchProvider.cMatchProvider_SQL2 ( 199 queries = [u""" 200 select * 201 from ( 202 select distinct on (comment) * 203 from ( 204 -- keyed by doc type 205 select comment, comment as pk, 1 as rank 206 from blobs.doc_med 207 where 208 comment %(fragment_condition)s 209 %(ctxt_doc_type)s 210 211 union all 212 213 select comment, comment as pk, 2 as rank 214 from blobs.doc_med 215 where comment %(fragment_condition)s 216 ) as q_union 217 ) as q_distinct 218 order by rank, comment 219 limit 25"""], 220 context = context 221 ) 222 mp.setThresholds(3, 5, 7) 223 mp.unset_context(u'pk_doc_type') 224 225 self.matcher = mp 226 self.picklist_delay = 50 227 228 self.SetToolTipString(_('Enter a comment on the document.'))
229 #============================================================ 230 # document type widgets 231 #============================================================
232 -def manage_document_types(parent=None):
233 234 if parent is None: 235 parent = wx.GetApp().GetTopWindow() 236 237 #dlg = gmDocumentWidgets.cEditDocumentTypesDlg(parent = self, id=-1) 238 dlg = cEditDocumentTypesDlg(parent = parent) 239 dlg.ShowModal()
240 #============================================================ 241 from Gnumed.wxGladeWidgets import wxgEditDocumentTypesDlg 242
243 -class cEditDocumentTypesDlg(wxgEditDocumentTypesDlg.wxgEditDocumentTypesDlg):
244 """A dialog showing a cEditDocumentTypesPnl.""" 245
246 - def __init__(self, *args, **kwargs):
248 249 #============================================================ 250 from Gnumed.wxGladeWidgets import wxgEditDocumentTypesPnl 251
252 -class cEditDocumentTypesPnl(wxgEditDocumentTypesPnl.wxgEditDocumentTypesPnl):
253 """A panel grouping together fields to edit the list of document types.""" 254
255 - def __init__(self, *args, **kwargs):
256 wxgEditDocumentTypesPnl.wxgEditDocumentTypesPnl.__init__(self, *args, **kwargs) 257 self.__init_ui() 258 self.__register_interests() 259 self.repopulate_ui()
260 #--------------------------------------------------------
261 - def __init_ui(self):
262 self._LCTRL_doc_type.set_columns([_('Type'), _('Translation'), _('User defined'), _('In use')]) 263 self._LCTRL_doc_type.set_column_widths()
264 #--------------------------------------------------------
265 - def __register_interests(self):
266 gmDispatcher.connect(signal = u'doc_type_mod_db', receiver = self._on_doc_type_mod_db)
267 #--------------------------------------------------------
268 - def _on_doc_type_mod_db(self):
269 wx.CallAfter(self.repopulate_ui)
270 #--------------------------------------------------------
271 - def repopulate_ui(self):
272 273 self._LCTRL_doc_type.DeleteAllItems() 274 275 doc_types = gmDocuments.get_document_types() 276 pos = len(doc_types) + 1 277 278 for doc_type in doc_types: 279 row_num = self._LCTRL_doc_type.InsertStringItem(pos, label = doc_type['type']) 280 self._LCTRL_doc_type.SetStringItem(index = row_num, col = 1, label = doc_type['l10n_type']) 281 if doc_type['is_user_defined']: 282 self._LCTRL_doc_type.SetStringItem(index = row_num, col = 2, label = ' X ') 283 if doc_type['is_in_use']: 284 self._LCTRL_doc_type.SetStringItem(index = row_num, col = 3, label = ' X ') 285 286 if len(doc_types) > 0: 287 self._LCTRL_doc_type.set_data(data = doc_types) 288 self._LCTRL_doc_type.SetColumnWidth(col=0, width=wx.LIST_AUTOSIZE) 289 self._LCTRL_doc_type.SetColumnWidth(col=1, width=wx.LIST_AUTOSIZE) 290 self._LCTRL_doc_type.SetColumnWidth(col=2, width=wx.LIST_AUTOSIZE_USEHEADER) 291 self._LCTRL_doc_type.SetColumnWidth(col=3, width=wx.LIST_AUTOSIZE_USEHEADER) 292 293 self._TCTRL_type.SetValue('') 294 self._TCTRL_l10n_type.SetValue('') 295 296 self._BTN_set_translation.Enable(False) 297 self._BTN_delete.Enable(False) 298 self._BTN_add.Enable(False) 299 self._BTN_reassign.Enable(False) 300 301 self._LCTRL_doc_type.SetFocus()
302 #-------------------------------------------------------- 303 # event handlers 304 #--------------------------------------------------------
305 - def _on_list_item_selected(self, evt):
306 doc_type = self._LCTRL_doc_type.get_selected_item_data() 307 308 self._TCTRL_type.SetValue(doc_type['type']) 309 self._TCTRL_l10n_type.SetValue(doc_type['l10n_type']) 310 311 self._BTN_set_translation.Enable(True) 312 self._BTN_delete.Enable(not bool(doc_type['is_in_use'])) 313 self._BTN_add.Enable(False) 314 self._BTN_reassign.Enable(True) 315 316 return
317 #--------------------------------------------------------
318 - def _on_type_modified(self, event):
319 self._BTN_set_translation.Enable(False) 320 self._BTN_delete.Enable(False) 321 self._BTN_reassign.Enable(False) 322 323 self._BTN_add.Enable(True) 324 # self._LCTRL_doc_type.deselect_selected_item() 325 return
326 #--------------------------------------------------------
327 - def _on_set_translation_button_pressed(self, event):
328 doc_type = self._LCTRL_doc_type.get_selected_item_data() 329 if doc_type.set_translation(translation = self._TCTRL_l10n_type.GetValue().strip()): 330 wx.CallAfter(self.repopulate_ui) 331 332 return
333 #--------------------------------------------------------
334 - def _on_delete_button_pressed(self, event):
335 doc_type = self._LCTRL_doc_type.get_selected_item_data() 336 if doc_type['is_in_use']: 337 gmGuiHelpers.gm_show_info ( 338 _( 339 'Cannot delete document type\n' 340 ' [%s]\n' 341 'because it is currently in use.' 342 ) % doc_type['l10n_type'], 343 _('deleting document type') 344 ) 345 return 346 347 gmDocuments.delete_document_type(document_type = doc_type) 348 349 return
350 #--------------------------------------------------------
351 - def _on_add_button_pressed(self, event):
352 desc = self._TCTRL_type.GetValue().strip() 353 if desc != '': 354 doc_type = gmDocuments.create_document_type(document_type = desc) # does not create dupes 355 l10n_desc = self._TCTRL_l10n_type.GetValue().strip() 356 if (l10n_desc != '') and (l10n_desc != doc_type['l10n_type']): 357 doc_type.set_translation(translation = l10n_desc) 358 359 return
360 #--------------------------------------------------------
361 - def _on_reassign_button_pressed(self, event):
362 363 orig_type = self._LCTRL_doc_type.get_selected_item_data() 364 doc_types = gmDocuments.get_document_types() 365 366 new_type = gmListWidgets.get_choices_from_list ( 367 parent = self, 368 msg = _( 369 'From the list below select the document type you want\n' 370 'all documents currently classified as:\n\n' 371 ' "%s"\n\n' 372 'to be changed to.\n\n' 373 'Be aware that this change will be applied to ALL such documents. If there\n' 374 'are many documents to change it can take quite a while.\n\n' 375 'Make sure this is what you want to happen !\n' 376 ) % orig_type['l10n_type'], 377 caption = _('Reassigning document type'), 378 choices = [ [gmTools.bool2subst(dt['is_user_defined'], u'X', u''), dt['type'], dt['l10n_type']] for dt in doc_types ], 379 columns = [_('User defined'), _('Type'), _('Translation')], 380 data = doc_types, 381 single_selection = True 382 ) 383 384 if new_type is None: 385 return 386 387 wx.BeginBusyCursor() 388 gmDocuments.reclassify_documents_by_type(original_type = orig_type, target_type = new_type) 389 wx.EndBusyCursor() 390 391 return
392 #============================================================
393 -class cDocumentTypeSelectionPhraseWheel(gmPhraseWheel.cPhraseWheel):
394 """Let user select a document type."""
395 - def __init__(self, *args, **kwargs):
396 397 gmPhraseWheel.cPhraseWheel.__init__(self, *args, **kwargs) 398 399 mp = gmMatchProvider.cMatchProvider_SQL2 ( 400 queries = [ 401 u"""SELECT * FROM (( 402 SELECT 403 pk_doc_type AS data, 404 l10n_type AS field_label, 405 l10n_type AS list_label, 406 1 AS rank 407 FROM blobs.v_doc_type 408 WHERE 409 is_user_defined IS True 410 AND 411 l10n_type %(fragment_condition)s 412 ) UNION ( 413 SELECT 414 pk_doc_type AS data, 415 l10n_type AS field_label, 416 l10n_type AS list_label, 417 2 AS rank 418 FROM blobs.v_doc_type 419 WHERE 420 is_user_defined IS False 421 AND 422 l10n_type %(fragment_condition)s 423 )) AS q1 424 ORDER BY q1.rank, q1.field_label"""] 425 ) 426 mp.setThresholds(2, 4, 6) 427 428 self.matcher = mp 429 self.picklist_delay = 50 430 431 self.SetToolTipString(_('Select the document type.'))
432 #--------------------------------------------------------
433 - def _create_data(self):
434 435 doc_type = self.GetValue().strip() 436 if doc_type == u'': 437 gmDispatcher.send(signal = u'statustext', msg = _('Cannot create document type without name.'), beep = True) 438 _log.debug('cannot create document type without name') 439 return 440 441 pk = gmDocuments.create_document_type(doc_type)['pk_doc_type'] 442 if pk is None: 443 self.data = {} 444 else: 445 self.SetText ( 446 value = doc_type, 447 data = pk 448 )
449 #============================================================ 450 from Gnumed.wxGladeWidgets import wxgReviewDocPartDlg 451
452 -class cReviewDocPartDlg(wxgReviewDocPartDlg.wxgReviewDocPartDlg):
453 - def __init__(self, *args, **kwds):
454 """Support parts and docs now. 455 """ 456 part = kwds['part'] 457 del kwds['part'] 458 wxgReviewDocPartDlg.wxgReviewDocPartDlg.__init__(self, *args, **kwds) 459 460 if isinstance(part, gmDocuments.cDocumentPart): 461 self.__part = part 462 self.__doc = self.__part.get_containing_document() 463 self.__reviewing_doc = False 464 elif isinstance(part, gmDocuments.cDocument): 465 self.__doc = part 466 self.__part = self.__doc.parts[0] 467 self.__reviewing_doc = True 468 else: 469 raise ValueError('<part> must be gmDocuments.cDocument or gmDocuments.cDocumentPart instance, got <%s>' % type(part)) 470 471 self.__init_ui_data()
472 #-------------------------------------------------------- 473 # internal API 474 #--------------------------------------------------------
475 - def __init_ui_data(self):
476 # FIXME: fix this 477 # associated episode (add " " to avoid popping up pick list) 478 self._PhWheel_episode.SetText('%s ' % self.__part['episode'], self.__part['pk_episode']) 479 self._PhWheel_doc_type.SetText(value = self.__part['l10n_type'], data = self.__part['pk_type']) 480 self._PhWheel_doc_type.add_callback_on_set_focus(self._on_doc_type_gets_focus) 481 self._PhWheel_doc_type.add_callback_on_lose_focus(self._on_doc_type_loses_focus) 482 483 if self.__reviewing_doc: 484 self._PRW_doc_comment.SetText(gmTools.coalesce(self.__part['doc_comment'], '')) 485 self._PRW_doc_comment.set_context(context = 'pk_doc_type', val = self.__part['pk_type']) 486 else: 487 self._PRW_doc_comment.SetText(gmTools.coalesce(self.__part['obj_comment'], '')) 488 489 fts = gmDateTime.cFuzzyTimestamp(timestamp = self.__part['date_generated']) 490 self._PhWheel_doc_date.SetText(fts.strftime('%Y-%m-%d'), fts) 491 self._TCTRL_reference.SetValue(gmTools.coalesce(self.__part['ext_ref'], '')) 492 if self.__reviewing_doc: 493 self._TCTRL_filename.Enable(False) 494 self._SPINCTRL_seq_idx.Enable(False) 495 else: 496 self._TCTRL_filename.SetValue(gmTools.coalesce(self.__part['filename'], '')) 497 self._SPINCTRL_seq_idx.SetValue(gmTools.coalesce(self.__part['seq_idx'], 0)) 498 499 self._LCTRL_existing_reviews.InsertColumn(0, _('who')) 500 self._LCTRL_existing_reviews.InsertColumn(1, _('when')) 501 self._LCTRL_existing_reviews.InsertColumn(2, _('+/-')) 502 self._LCTRL_existing_reviews.InsertColumn(3, _('!')) 503 self._LCTRL_existing_reviews.InsertColumn(4, _('comment')) 504 505 self.__reload_existing_reviews() 506 507 if self._LCTRL_existing_reviews.GetItemCount() > 0: 508 self._LCTRL_existing_reviews.SetColumnWidth(col=0, width=wx.LIST_AUTOSIZE) 509 self._LCTRL_existing_reviews.SetColumnWidth(col=1, width=wx.LIST_AUTOSIZE) 510 self._LCTRL_existing_reviews.SetColumnWidth(col=2, width=wx.LIST_AUTOSIZE_USEHEADER) 511 self._LCTRL_existing_reviews.SetColumnWidth(col=3, width=wx.LIST_AUTOSIZE_USEHEADER) 512 self._LCTRL_existing_reviews.SetColumnWidth(col=4, width=wx.LIST_AUTOSIZE) 513 514 me = gmPerson.gmCurrentProvider() 515 if self.__part['pk_intended_reviewer'] == me['pk_staff']: 516 msg = _('(you are the primary reviewer)') 517 else: 518 msg = _('(someone else is the primary reviewer)') 519 self._TCTRL_responsible.SetValue(msg) 520 521 # init my review if any 522 if self.__part['reviewed_by_you']: 523 revs = self.__part.get_reviews() 524 for rev in revs: 525 if rev['is_your_review']: 526 self._ChBOX_abnormal.SetValue(bool(rev[2])) 527 self._ChBOX_relevant.SetValue(bool(rev[3])) 528 break 529 530 self._ChBOX_sign_all_pages.SetValue(self.__reviewing_doc) 531 532 return True
533 #--------------------------------------------------------
534 - def __reload_existing_reviews(self):
535 self._LCTRL_existing_reviews.DeleteAllItems() 536 revs = self.__part.get_reviews() # FIXME: this is ugly as sin, it should be dicts, not lists 537 if len(revs) == 0: 538 return True 539 # find special reviews 540 review_by_responsible_doc = None 541 reviews_by_others = [] 542 for rev in revs: 543 if rev['is_review_by_responsible_reviewer'] and not rev['is_your_review']: 544 review_by_responsible_doc = rev 545 if not (rev['is_review_by_responsible_reviewer'] or rev['is_your_review']): 546 reviews_by_others.append(rev) 547 # display them 548 if review_by_responsible_doc is not None: 549 row_num = self._LCTRL_existing_reviews.InsertStringItem(sys.maxint, label=review_by_responsible_doc[0]) 550 self._LCTRL_existing_reviews.SetItemTextColour(row_num, col=wx.BLUE) 551 self._LCTRL_existing_reviews.SetStringItem(index = row_num, col=0, label=review_by_responsible_doc[0]) 552 self._LCTRL_existing_reviews.SetStringItem(index = row_num, col=1, label=review_by_responsible_doc[1].strftime('%x %H:%M')) 553 if review_by_responsible_doc['is_technically_abnormal']: 554 self._LCTRL_existing_reviews.SetStringItem(index = row_num, col=2, label=u'X') 555 if review_by_responsible_doc['clinically_relevant']: 556 self._LCTRL_existing_reviews.SetStringItem(index = row_num, col=3, label=u'X') 557 self._LCTRL_existing_reviews.SetStringItem(index = row_num, col=4, label=review_by_responsible_doc[6]) 558 row_num += 1 559 for rev in reviews_by_others: 560 row_num = self._LCTRL_existing_reviews.InsertStringItem(sys.maxint, label=rev[0]) 561 self._LCTRL_existing_reviews.SetStringItem(index = row_num, col=0, label=rev[0]) 562 self._LCTRL_existing_reviews.SetStringItem(index = row_num, col=1, label=rev[1].strftime('%x %H:%M')) 563 if rev['is_technically_abnormal']: 564 self._LCTRL_existing_reviews.SetStringItem(index = row_num, col=2, label=u'X') 565 if rev['clinically_relevant']: 566 self._LCTRL_existing_reviews.SetStringItem(index = row_num, col=3, label=u'X') 567 self._LCTRL_existing_reviews.SetStringItem(index = row_num, col=4, label=rev[6]) 568 return True
569 #-------------------------------------------------------- 570 # event handlers 571 #--------------------------------------------------------
572 - def _on_save_button_pressed(self, evt):
573 """Save the metadata to the backend.""" 574 575 evt.Skip() 576 577 # 1) handle associated episode 578 pk_episode = self._PhWheel_episode.GetData(can_create=True, is_open=True) 579 if pk_episode is None: 580 gmGuiHelpers.gm_show_error ( 581 _('Cannot create episode\n [%s]'), 582 _('Editing document properties') 583 ) 584 return False 585 586 doc_type = self._PhWheel_doc_type.GetData(can_create = True) 587 if doc_type is None: 588 gmDispatcher.send(signal='statustext', msg=_('Cannot change document type to [%s].') % self._PhWheel_doc_type.GetValue().strip()) 589 return False 590 591 # since the phrasewheel operates on the active 592 # patient all episodes really should belong 593 # to it so we don't check patient change 594 self.__doc['pk_episode'] = pk_episode 595 self.__doc['pk_type'] = doc_type 596 if self.__reviewing_doc: 597 self.__doc['comment'] = self._PRW_doc_comment.GetValue().strip() 598 self.__doc['clin_when'] = self._PhWheel_doc_date.GetData().get_pydt() 599 self.__doc['ext_ref'] = self._TCTRL_reference.GetValue().strip() 600 601 success, data = self.__doc.save_payload() 602 if not success: 603 gmGuiHelpers.gm_show_error ( 604 _('Cannot link the document to episode\n\n [%s]') % epi_name, 605 _('Editing document properties') 606 ) 607 return False 608 609 # 2) handle review 610 if self._ChBOX_review.GetValue(): 611 provider = gmPerson.gmCurrentProvider() 612 abnormal = self._ChBOX_abnormal.GetValue() 613 relevant = self._ChBOX_relevant.GetValue() 614 msg = None 615 if self.__reviewing_doc: # - on all pages 616 if not self.__doc.set_reviewed(technically_abnormal = abnormal, clinically_relevant = relevant): 617 msg = _('Error setting "reviewed" status of this document.') 618 if self._ChBOX_responsible.GetValue(): 619 if not self.__doc.set_primary_reviewer(reviewer = provider['pk_staff']): 620 msg = _('Error setting responsible clinician for this document.') 621 else: # - just on this page 622 if not self.__part.set_reviewed(technically_abnormal = abnormal, clinically_relevant = relevant): 623 msg = _('Error setting "reviewed" status of this part.') 624 if self._ChBOX_responsible.GetValue(): 625 self.__part['pk_intended_reviewer'] = provider['pk_staff'] 626 if msg is not None: 627 gmGuiHelpers.gm_show_error(msg, _('Editing document properties')) 628 return False 629 630 # 3) handle "page" specific parts 631 if not self.__reviewing_doc: 632 self.__part['filename'] = gmTools.none_if(self._TCTRL_filename.GetValue().strip(), u'') 633 new_idx = gmTools.none_if(self._SPINCTRL_seq_idx.GetValue(), 0) 634 if new_idx in self.__doc['seq_idx_list']: 635 msg = _( 636 'Cannot set page number to [%s] because\n' 637 'another page with this number exists.\n' 638 '\n' 639 'Page numbers in use:\n' 640 '\n' 641 ' %s' 642 ) % ( 643 new_idx, 644 self.__doc['seq_idx_list'] 645 ) 646 gmGuiHelpers.gm_show_error(msg, _('Editing document part properties')) 647 else: 648 self.__part['seq_idx'] = new_idx 649 self.__part['obj_comment'] = self._PRW_doc_comment.GetValue().strip() 650 success, data = self.__part.save_payload() 651 if not success: 652 gmGuiHelpers.gm_show_error ( 653 _('Error saving part properties.'), 654 _('Editing document part properties') 655 ) 656 return False 657 658 return True
659 #--------------------------------------------------------
660 - def _on_reviewed_box_checked(self, evt):
661 state = self._ChBOX_review.GetValue() 662 self._ChBOX_abnormal.Enable(enable = state) 663 self._ChBOX_relevant.Enable(enable = state) 664 self._ChBOX_responsible.Enable(enable = state)
665 #--------------------------------------------------------
666 - def _on_doc_type_gets_focus(self):
667 """Per Jim: Changing the doc type happens a lot more often 668 then correcting spelling, hence select-all on getting focus. 669 """ 670 self._PhWheel_doc_type.SetSelection(-1, -1)
671 #--------------------------------------------------------
672 - def _on_doc_type_loses_focus(self):
673 pk_doc_type = self._PhWheel_doc_type.GetData() 674 if pk_doc_type is None: 675 self._PRW_doc_comment.unset_context(context = 'pk_doc_type') 676 else: 677 self._PRW_doc_comment.set_context(context = 'pk_doc_type', val = pk_doc_type) 678 return True
679 #============================================================
680 -def acquire_images_from_capture_device(device=None, calling_window=None):
681 682 _log.debug('acquiring images from [%s]', device) 683 684 # do not import globally since we might want to use 685 # this module without requiring any scanner to be available 686 from Gnumed.pycommon import gmScanBackend 687 try: 688 fnames = gmScanBackend.acquire_pages_into_files ( 689 device = device, 690 delay = 5, 691 calling_window = calling_window 692 ) 693 except OSError: 694 _log.exception('problem acquiring image from source') 695 gmGuiHelpers.gm_show_error ( 696 aMessage = _( 697 'No images could be acquired from the source.\n\n' 698 'This may mean the scanner driver is not properly installed.\n\n' 699 'On Windows you must install the TWAIN Python module\n' 700 'while on Linux and MacOSX it is recommended to install\n' 701 'the XSane package.' 702 ), 703 aTitle = _('Acquiring images') 704 ) 705 return None 706 707 _log.debug('acquired %s images', len(fnames)) 708 709 return fnames
710 #------------------------------------------------------------ 711 from Gnumed.wxGladeWidgets import wxgScanIdxPnl 712
713 -class cScanIdxDocsPnl(wxgScanIdxPnl.wxgScanIdxPnl, gmPlugin.cPatientChange_PluginMixin):
714 - def __init__(self, *args, **kwds):
715 wxgScanIdxPnl.wxgScanIdxPnl.__init__(self, *args, **kwds) 716 gmPlugin.cPatientChange_PluginMixin.__init__(self) 717 718 self._PhWheel_reviewer.matcher = gmPerson.cMatchProvider_Provider() 719 720 self.__init_ui_data() 721 self._PhWheel_doc_type.add_callback_on_lose_focus(self._on_doc_type_loses_focus) 722 723 # make me and listctrl a file drop target 724 dt = gmGuiHelpers.cFileDropTarget(self) 725 self.SetDropTarget(dt) 726 dt = gmGuiHelpers.cFileDropTarget(self._LBOX_doc_pages) 727 self._LBOX_doc_pages.SetDropTarget(dt) 728 self._LBOX_doc_pages.add_filenames = self.add_filenames_to_listbox 729 730 # do not import globally since we might want to use 731 # this module without requiring any scanner to be available 732 from Gnumed.pycommon import gmScanBackend 733 self.scan_module = gmScanBackend
734 #-------------------------------------------------------- 735 # file drop target API 736 #--------------------------------------------------------
737 - def add_filenames_to_listbox(self, filenames):
738 self.add_filenames(filenames=filenames)
739 #--------------------------------------------------------
740 - def add_filenames(self, filenames):
741 pat = gmPerson.gmCurrentPatient() 742 if not pat.connected: 743 gmDispatcher.send(signal='statustext', msg=_('Cannot accept new documents. No active patient.')) 744 return 745 746 # dive into folders dropped onto us and extract files (one level deep only) 747 real_filenames = [] 748 for pathname in filenames: 749 try: 750 files = os.listdir(pathname) 751 gmDispatcher.send(signal='statustext', msg=_('Extracting files from folder [%s] ...') % pathname) 752 for file in files: 753 fullname = os.path.join(pathname, file) 754 if not os.path.isfile(fullname): 755 continue 756 real_filenames.append(fullname) 757 except OSError: 758 real_filenames.append(pathname) 759 760 self.acquired_pages.extend(real_filenames) 761 self.__reload_LBOX_doc_pages()
762 #--------------------------------------------------------
763 - def repopulate_ui(self):
764 pass
765 #-------------------------------------------------------- 766 # patient change plugin API 767 #--------------------------------------------------------
768 - def _pre_patient_selection(self, **kwds):
769 # FIXME: persist pending data from here 770 pass
771 #--------------------------------------------------------
772 - def _post_patient_selection(self, **kwds):
773 self.__init_ui_data()
774 #-------------------------------------------------------- 775 # internal API 776 #--------------------------------------------------------
777 - def __init_ui_data(self):
778 # ----------------------------- 779 self._PhWheel_episode.SetText('') 780 self._PhWheel_doc_type.SetText('') 781 # ----------------------------- 782 # FIXME: make this configurable: either now() or last_date() 783 fts = gmDateTime.cFuzzyTimestamp() 784 self._PhWheel_doc_date.SetText(fts.strftime('%Y-%m-%d'), fts) 785 self._PRW_doc_comment.SetText('') 786 # FIXME: should be set to patient's primary doc 787 self._PhWheel_reviewer.selection_only = True 788 me = gmPerson.gmCurrentProvider() 789 self._PhWheel_reviewer.SetText ( 790 value = u'%s (%s%s %s)' % (me['short_alias'], me['title'], me['firstnames'], me['lastnames']), 791 data = me['pk_staff'] 792 ) 793 # ----------------------------- 794 # FIXME: set from config item 795 self._ChBOX_reviewed.SetValue(False) 796 self._ChBOX_abnormal.Disable() 797 self._ChBOX_abnormal.SetValue(False) 798 self._ChBOX_relevant.Disable() 799 self._ChBOX_relevant.SetValue(False) 800 # ----------------------------- 801 self._TBOX_description.SetValue('') 802 # ----------------------------- 803 # the list holding our page files 804 self._LBOX_doc_pages.Clear() 805 self.acquired_pages = []
806 #--------------------------------------------------------
807 - def __reload_LBOX_doc_pages(self):
808 self._LBOX_doc_pages.Clear() 809 if len(self.acquired_pages) > 0: 810 for i in range(len(self.acquired_pages)): 811 fname = self.acquired_pages[i] 812 self._LBOX_doc_pages.Append(_('part %s: %s') % (i+1, fname), fname)
813 #--------------------------------------------------------
814 - def __valid_for_save(self):
815 title = _('saving document') 816 817 if self.acquired_pages is None or len(self.acquired_pages) == 0: 818 dbcfg = gmCfg.cCfgSQL() 819 allow_empty = bool(dbcfg.get2 ( 820 option = u'horstspace.scan_index.allow_partless_documents', 821 workplace = gmSurgery.gmCurrentPractice().active_workplace, 822 bias = 'user', 823 default = False 824 )) 825 if allow_empty: 826 save_empty = gmGuiHelpers.gm_show_question ( 827 aMessage = _('No parts to save. Really save an empty document as a reference ?'), 828 aTitle = title 829 ) 830 if not save_empty: 831 return False 832 else: 833 gmGuiHelpers.gm_show_error ( 834 aMessage = _('No parts to save. Aquire some parts first.'), 835 aTitle = title 836 ) 837 return False 838 839 doc_type_pk = self._PhWheel_doc_type.GetData(can_create = True) 840 if doc_type_pk is None: 841 gmGuiHelpers.gm_show_error ( 842 aMessage = _('No document type applied. Choose a document type'), 843 aTitle = title 844 ) 845 return False 846 847 # this should be optional, actually 848 # if self._PRW_doc_comment.GetValue().strip() == '': 849 # gmGuiHelpers.gm_show_error ( 850 # aMessage = _('No document comment supplied. Add a comment for this document.'), 851 # aTitle = title 852 # ) 853 # return False 854 855 if self._PhWheel_episode.GetValue().strip() == '': 856 gmGuiHelpers.gm_show_error ( 857 aMessage = _('You must select an episode to save this document under.'), 858 aTitle = title 859 ) 860 return False 861 862 if self._PhWheel_reviewer.GetData() is None: 863 gmGuiHelpers.gm_show_error ( 864 aMessage = _('You need to select from the list of staff members the doctor who is intended to sign the document.'), 865 aTitle = title 866 ) 867 return False 868 869 return True
870 #--------------------------------------------------------
871 - def get_device_to_use(self, reconfigure=False):
872 873 if not reconfigure: 874 dbcfg = gmCfg.cCfgSQL() 875 device = dbcfg.get2 ( 876 option = 'external.xsane.default_device', 877 workplace = gmSurgery.gmCurrentPractice().active_workplace, 878 bias = 'workplace', 879 default = '' 880 ) 881 if device.strip() == u'': 882 device = None 883 if device is not None: 884 return device 885 886 try: 887 devices = self.scan_module.get_devices() 888 except: 889 _log.exception('cannot retrieve list of image sources') 890 gmDispatcher.send(signal = 'statustext', msg = _('There is no scanner support installed on this machine.')) 891 return None 892 893 if devices is None: 894 # get_devices() not implemented for TWAIN yet 895 # XSane has its own chooser (so does TWAIN) 896 return None 897 898 if len(devices) == 0: 899 gmDispatcher.send(signal = 'statustext', msg = _('Cannot find an active scanner.')) 900 return None 901 902 # device_names = [] 903 # for device in devices: 904 # device_names.append('%s (%s)' % (device[2], device[0])) 905 906 device = gmListWidgets.get_choices_from_list ( 907 parent = self, 908 msg = _('Select an image capture device'), 909 caption = _('device selection'), 910 choices = [ '%s (%s)' % (d[2], d[0]) for d in devices ], 911 columns = [_('Device')], 912 data = devices, 913 single_selection = True 914 ) 915 if device is None: 916 return None 917 918 # FIXME: add support for actually reconfiguring 919 return device[0]
920 #-------------------------------------------------------- 921 # event handling API 922 #--------------------------------------------------------
923 - def _scan_btn_pressed(self, evt):
924 925 chosen_device = self.get_device_to_use() 926 927 tmpdir = os.path.expanduser(os.path.join('~', '.gnumed', 'tmp')) 928 try: 929 gmTools.mkdir(tmpdir) 930 except: 931 tmpdir = None 932 933 # FIXME: configure whether to use XSane or sane directly 934 # FIXME: add support for xsane_device_settings argument 935 try: 936 fnames = self.scan_module.acquire_pages_into_files ( 937 device = chosen_device, 938 delay = 5, 939 tmpdir = tmpdir, 940 calling_window = self 941 ) 942 except OSError: 943 _log.exception('problem acquiring image from source') 944 gmGuiHelpers.gm_show_error ( 945 aMessage = _( 946 'No pages could be acquired from the source.\n\n' 947 'This may mean the scanner driver is not properly installed.\n\n' 948 'On Windows you must install the TWAIN Python module\n' 949 'while on Linux and MacOSX it is recommended to install\n' 950 'the XSane package.' 951 ), 952 aTitle = _('acquiring page') 953 ) 954 return None 955 956 if len(fnames) == 0: # no pages scanned 957 return True 958 959 self.acquired_pages.extend(fnames) 960 self.__reload_LBOX_doc_pages() 961 962 return True
963 #--------------------------------------------------------
964 - def _load_btn_pressed(self, evt):
965 # patient file chooser 966 dlg = wx.FileDialog ( 967 parent = None, 968 message = _('Choose a file'), 969 defaultDir = os.path.expanduser(os.path.join('~', 'gnumed')), 970 defaultFile = '', 971 wildcard = "%s (*)|*|TIFFs (*.tif)|*.tif|JPEGs (*.jpg)|*.jpg|%s (*.*)|*.*" % (_('all files'), _('all files (Win)')), 972 style = wx.OPEN | wx.HIDE_READONLY | wx.FILE_MUST_EXIST | wx.MULTIPLE 973 ) 974 result = dlg.ShowModal() 975 if result != wx.ID_CANCEL: 976 files = dlg.GetPaths() 977 for file in files: 978 self.acquired_pages.append(file) 979 self.__reload_LBOX_doc_pages() 980 dlg.Destroy()
981 #--------------------------------------------------------
982 - def _show_btn_pressed(self, evt):
983 # did user select a page ? 984 page_idx = self._LBOX_doc_pages.GetSelection() 985 if page_idx == -1: 986 gmGuiHelpers.gm_show_info ( 987 aMessage = _('You must select a part before you can view it.'), 988 aTitle = _('displaying part') 989 ) 990 return None 991 # now, which file was that again ? 992 page_fname = self._LBOX_doc_pages.GetClientData(page_idx) 993 994 (result, msg) = gmMimeLib.call_viewer_on_file(page_fname) 995 if not result: 996 gmGuiHelpers.gm_show_warning ( 997 aMessage = _('Cannot display document part:\n%s') % msg, 998 aTitle = _('displaying part') 999 ) 1000 return None 1001 return 1
1002 #--------------------------------------------------------
1003 - def _del_btn_pressed(self, event):
1004 page_idx = self._LBOX_doc_pages.GetSelection() 1005 if page_idx == -1: 1006 gmGuiHelpers.gm_show_info ( 1007 aMessage = _('You must select a part before you can delete it.'), 1008 aTitle = _('deleting part') 1009 ) 1010 return None 1011 page_fname = self._LBOX_doc_pages.GetClientData(page_idx) 1012 1013 # 1) del item from self.acquired_pages 1014 self.acquired_pages[page_idx:(page_idx+1)] = [] 1015 1016 # 2) reload list box 1017 self.__reload_LBOX_doc_pages() 1018 1019 # 3) optionally kill file in the file system 1020 do_delete = gmGuiHelpers.gm_show_question ( 1021 _('The part has successfully been removed from the document.\n' 1022 '\n' 1023 'Do you also want to permanently delete the file\n' 1024 '\n' 1025 ' [%s]\n' 1026 '\n' 1027 'from which this document part was loaded ?\n' 1028 '\n' 1029 'If it is a temporary file for a page you just scanned\n' 1030 'this makes a lot of sense. In other cases you may not\n' 1031 'want to lose the file.\n' 1032 '\n' 1033 'Pressing [YES] will permanently remove the file\n' 1034 'from your computer.\n' 1035 ) % page_fname, 1036 _('Removing document part') 1037 ) 1038 if do_delete: 1039 try: 1040 os.remove(page_fname) 1041 except: 1042 _log.exception('Error deleting file.') 1043 gmGuiHelpers.gm_show_error ( 1044 aMessage = _('Cannot delete part in file [%s].\n\nYou may not have write access to it.') % page_fname, 1045 aTitle = _('deleting part') 1046 ) 1047 1048 return 1
1049 #--------------------------------------------------------
1050 - def _save_btn_pressed(self, evt):
1051 1052 if not self.__valid_for_save(): 1053 return False 1054 1055 wx.BeginBusyCursor() 1056 1057 pat = gmPerson.gmCurrentPatient() 1058 doc_folder = pat.get_document_folder() 1059 emr = pat.get_emr() 1060 1061 # create new document 1062 pk_episode = self._PhWheel_episode.GetData() 1063 if pk_episode is None: 1064 episode = emr.add_episode ( 1065 episode_name = self._PhWheel_episode.GetValue().strip(), 1066 is_open = True 1067 ) 1068 if episode is None: 1069 wx.EndBusyCursor() 1070 gmGuiHelpers.gm_show_error ( 1071 aMessage = _('Cannot start episode [%s].') % self._PhWheel_episode.GetValue().strip(), 1072 aTitle = _('saving document') 1073 ) 1074 return False 1075 pk_episode = episode['pk_episode'] 1076 1077 encounter = emr.active_encounter['pk_encounter'] 1078 document_type = self._PhWheel_doc_type.GetData() 1079 new_doc = doc_folder.add_document(document_type, encounter, pk_episode) 1080 if new_doc is None: 1081 wx.EndBusyCursor() 1082 gmGuiHelpers.gm_show_error ( 1083 aMessage = _('Cannot create new document.'), 1084 aTitle = _('saving document') 1085 ) 1086 return False 1087 1088 # update business object with metadata 1089 # - date of generation 1090 new_doc['clin_when'] = self._PhWheel_doc_date.GetData().get_pydt() 1091 # - external reference 1092 cfg = gmCfg.cCfgSQL() 1093 generate_uuid = bool ( 1094 cfg.get2 ( 1095 option = 'horstspace.scan_index.generate_doc_uuid', 1096 workplace = gmSurgery.gmCurrentPractice().active_workplace, 1097 bias = 'user', 1098 default = False 1099 ) 1100 ) 1101 ref = None 1102 if generate_uuid: 1103 ref = gmDocuments.get_ext_ref() 1104 if ref is not None: 1105 new_doc['ext_ref'] = ref 1106 # - comment 1107 comment = self._PRW_doc_comment.GetLineText(0).strip() 1108 if comment != u'': 1109 new_doc['comment'] = comment 1110 # - save it 1111 if not new_doc.save_payload(): 1112 wx.EndBusyCursor() 1113 gmGuiHelpers.gm_show_error ( 1114 aMessage = _('Cannot update document metadata.'), 1115 aTitle = _('saving document') 1116 ) 1117 return False 1118 # - long description 1119 description = self._TBOX_description.GetValue().strip() 1120 if description != '': 1121 if not new_doc.add_description(description): 1122 wx.EndBusyCursor() 1123 gmGuiHelpers.gm_show_error ( 1124 aMessage = _('Cannot add document description.'), 1125 aTitle = _('saving document') 1126 ) 1127 return False 1128 1129 # add document parts from files 1130 success, msg, filename = new_doc.add_parts_from_files ( 1131 files = self.acquired_pages, 1132 reviewer = self._PhWheel_reviewer.GetData() 1133 ) 1134 if not success: 1135 wx.EndBusyCursor() 1136 gmGuiHelpers.gm_show_error ( 1137 aMessage = msg, 1138 aTitle = _('saving document') 1139 ) 1140 return False 1141 1142 # set reviewed status 1143 if self._ChBOX_reviewed.GetValue(): 1144 if not new_doc.set_reviewed ( 1145 technically_abnormal = self._ChBOX_abnormal.GetValue(), 1146 clinically_relevant = self._ChBOX_relevant.GetValue() 1147 ): 1148 msg = _('Error setting "reviewed" status of new document.') 1149 1150 gmHooks.run_hook_script(hook = u'after_new_doc_created') 1151 1152 # inform user 1153 show_id = bool ( 1154 cfg.get2 ( 1155 option = 'horstspace.scan_index.show_doc_id', 1156 workplace = gmSurgery.gmCurrentPractice().active_workplace, 1157 bias = 'user' 1158 ) 1159 ) 1160 wx.EndBusyCursor() 1161 if show_id: 1162 if ref is None: 1163 msg = _('Successfully saved the new document.') 1164 else: 1165 msg = _( 1166 """The reference ID for the new document is: 1167 1168 <%s> 1169 1170 You probably want to write it down on the 1171 original documents. 1172 1173 If you don't care about the ID you can switch 1174 off this message in the GNUmed configuration.""") % ref 1175 gmGuiHelpers.gm_show_info ( 1176 aMessage = msg, 1177 aTitle = _('Saving document') 1178 ) 1179 else: 1180 gmDispatcher.send(signal='statustext', msg=_('Successfully saved new document.')) 1181 1182 self.__init_ui_data() 1183 return True
1184 #--------------------------------------------------------
1185 - def _startover_btn_pressed(self, evt):
1186 self.__init_ui_data()
1187 #--------------------------------------------------------
1188 - def _reviewed_box_checked(self, evt):
1189 self._ChBOX_abnormal.Enable(enable = self._ChBOX_reviewed.GetValue()) 1190 self._ChBOX_relevant.Enable(enable = self._ChBOX_reviewed.GetValue())
1191 #--------------------------------------------------------
1192 - def _on_doc_type_loses_focus(self):
1193 pk_doc_type = self._PhWheel_doc_type.GetData() 1194 if pk_doc_type is None: 1195 self._PRW_doc_comment.unset_context(context = 'pk_doc_type') 1196 else: 1197 self._PRW_doc_comment.set_context(context = 'pk_doc_type', val = pk_doc_type) 1198 return True
1199 #============================================================ 1200 from Gnumed.wxGladeWidgets import wxgSelectablySortedDocTreePnl 1201
1202 -class cSelectablySortedDocTreePnl(wxgSelectablySortedDocTreePnl.wxgSelectablySortedDocTreePnl):
1203 """A panel with a document tree which can be sorted.""" 1204 #-------------------------------------------------------- 1205 # inherited event handlers 1206 #--------------------------------------------------------
1207 - def _on_sort_by_age_selected(self, evt):
1208 self._doc_tree.sort_mode = 'age' 1209 self._doc_tree.SetFocus() 1210 self._rbtn_sort_by_age.SetValue(True)
1211 #--------------------------------------------------------
1212 - def _on_sort_by_review_selected(self, evt):
1213 self._doc_tree.sort_mode = 'review' 1214 self._doc_tree.SetFocus() 1215 self._rbtn_sort_by_review.SetValue(True)
1216 #--------------------------------------------------------
1217 - def _on_sort_by_episode_selected(self, evt):
1218 self._doc_tree.sort_mode = 'episode' 1219 self._doc_tree.SetFocus() 1220 self._rbtn_sort_by_episode.SetValue(True)
1221 #--------------------------------------------------------
1222 - def _on_sort_by_type_selected(self, evt):
1223 self._doc_tree.sort_mode = 'type' 1224 self._doc_tree.SetFocus() 1225 self._rbtn_sort_by_type.SetValue(True)
1226 #============================================================
1227 -class cDocTree(wx.TreeCtrl, gmRegetMixin.cRegetOnPaintMixin):
1228 # FIXME: handle expansion state 1229 """This wx.TreeCtrl derivative displays a tree view of stored medical documents. 1230 1231 It listens to document and patient changes and updated itself accordingly. 1232 1233 This acts on the current patient. 1234 """ 1235 _sort_modes = ['age', 'review', 'episode', 'type'] 1236 _root_node_labels = None 1237 #--------------------------------------------------------
1238 - def __init__(self, parent, id, *args, **kwds):
1239 """Set up our specialised tree. 1240 """ 1241 kwds['style'] = wx.TR_NO_BUTTONS | wx.NO_BORDER | wx.TR_SINGLE 1242 wx.TreeCtrl.__init__(self, parent, id, *args, **kwds) 1243 1244 gmRegetMixin.cRegetOnPaintMixin.__init__(self) 1245 1246 tmp = _('available documents (%s)') 1247 unsigned = _('unsigned (%s) on top') % u'\u270D' 1248 cDocTree._root_node_labels = { 1249 'age': tmp % _('most recent on top'), 1250 'review': tmp % unsigned, 1251 'episode': tmp % _('sorted by episode'), 1252 'type': tmp % _('sorted by type') 1253 } 1254 1255 self.root = None 1256 self.__sort_mode = 'age' 1257 1258 self.__build_context_menus() 1259 self.__register_interests() 1260 self._schedule_data_reget()
1261 #-------------------------------------------------------- 1262 # external API 1263 #--------------------------------------------------------
1264 - def display_selected_part(self, *args, **kwargs):
1265 1266 node = self.GetSelection() 1267 node_data = self.GetPyData(node) 1268 1269 if not isinstance(node_data, gmDocuments.cDocumentPart): 1270 return True 1271 1272 self.__display_part(part = node_data) 1273 return True
1274 #-------------------------------------------------------- 1275 # properties 1276 #--------------------------------------------------------
1277 - def _get_sort_mode(self):
1278 return self.__sort_mode
1279 #-----
1280 - def _set_sort_mode(self, mode):
1281 if mode is None: 1282 mode = 'age' 1283 1284 if mode == self.__sort_mode: 1285 return 1286 1287 if mode not in cDocTree._sort_modes: 1288 raise ValueError('invalid document tree sort mode [%s], valid modes: %s' % (mode, cDocTree._sort_modes)) 1289 1290 self.__sort_mode = mode 1291 1292 curr_pat = gmPerson.gmCurrentPatient() 1293 if not curr_pat.connected: 1294 return 1295 1296 self._schedule_data_reget()
1297 #----- 1298 sort_mode = property(_get_sort_mode, _set_sort_mode) 1299 #-------------------------------------------------------- 1300 # reget-on-paint API 1301 #--------------------------------------------------------
1302 - def _populate_with_data(self):
1303 curr_pat = gmPerson.gmCurrentPatient() 1304 if not curr_pat.connected: 1305 gmDispatcher.send(signal = 'statustext', msg = _('Cannot load documents. No active patient.')) 1306 return False 1307 1308 if not self.__populate_tree(): 1309 return False 1310 1311 return True
1312 #-------------------------------------------------------- 1313 # internal helpers 1314 #--------------------------------------------------------
1315 - def __register_interests(self):
1316 # connect handlers 1317 wx.EVT_TREE_ITEM_ACTIVATED (self, self.GetId(), self._on_activate) 1318 wx.EVT_TREE_ITEM_RIGHT_CLICK (self, self.GetId(), self.__on_right_click) 1319 1320 # wx.EVT_LEFT_DCLICK(self.tree, self.OnLeftDClick) 1321 1322 gmDispatcher.connect(signal = u'pre_patient_selection', receiver = self._on_pre_patient_selection) 1323 gmDispatcher.connect(signal = u'post_patient_selection', receiver = self._on_post_patient_selection) 1324 gmDispatcher.connect(signal = u'doc_mod_db', receiver = self._on_doc_mod_db) 1325 gmDispatcher.connect(signal = u'doc_page_mod_db', receiver = self._on_doc_page_mod_db)
1326 #--------------------------------------------------------
1327 - def __build_context_menus(self):
1328 1329 # --- part context menu --- 1330 self.__part_context_menu = wx.Menu(title = _('Part Actions:')) 1331 1332 ID = wx.NewId() 1333 self.__part_context_menu.Append(ID, _('Display part')) 1334 wx.EVT_MENU(self.__part_context_menu, ID, self.__display_curr_part) 1335 1336 ID = wx.NewId() 1337 self.__part_context_menu.Append(ID, _('%s Sign/Edit properties') % u'\u270D') 1338 wx.EVT_MENU(self.__part_context_menu, ID, self.__review_curr_part) 1339 1340 self.__part_context_menu.AppendSeparator() 1341 1342 ID = wx.NewId() 1343 self.__part_context_menu.Append(ID, _('Print part')) 1344 wx.EVT_MENU(self.__part_context_menu, ID, self.__print_part) 1345 1346 ID = wx.NewId() 1347 self.__part_context_menu.Append(ID, _('Fax part')) 1348 wx.EVT_MENU(self.__part_context_menu, ID, self.__fax_part) 1349 1350 ID = wx.NewId() 1351 self.__part_context_menu.Append(ID, _('Mail part')) 1352 wx.EVT_MENU(self.__part_context_menu, ID, self.__mail_part) 1353 1354 self.__part_context_menu.AppendSeparator() # so we can append some items 1355 1356 # --- doc context menu --- 1357 self.__doc_context_menu = wx.Menu(title = _('Document Actions:')) 1358 1359 ID = wx.NewId() 1360 self.__doc_context_menu.Append(ID, _('%s Sign/Edit properties') % u'\u270D') 1361 wx.EVT_MENU(self.__doc_context_menu, ID, self.__review_curr_part) 1362 1363 self.__doc_context_menu.AppendSeparator() 1364 1365 ID = wx.NewId() 1366 self.__doc_context_menu.Append(ID, _('Print all parts')) 1367 wx.EVT_MENU(self.__doc_context_menu, ID, self.__print_doc) 1368 1369 ID = wx.NewId() 1370 self.__doc_context_menu.Append(ID, _('Fax all parts')) 1371 wx.EVT_MENU(self.__doc_context_menu, ID, self.__fax_doc) 1372 1373 ID = wx.NewId() 1374 self.__doc_context_menu.Append(ID, _('Mail all parts')) 1375 wx.EVT_MENU(self.__doc_context_menu, ID, self.__mail_doc) 1376 1377 ID = wx.NewId() 1378 self.__doc_context_menu.Append(ID, _('Export all parts')) 1379 wx.EVT_MENU(self.__doc_context_menu, ID, self.__export_doc_to_disk) 1380 1381 self.__doc_context_menu.AppendSeparator() 1382 1383 ID = wx.NewId() 1384 self.__doc_context_menu.Append(ID, _('Delete document')) 1385 wx.EVT_MENU(self.__doc_context_menu, ID, self.__delete_document) 1386 1387 ID = wx.NewId() 1388 self.__doc_context_menu.Append(ID, _('Access external original')) 1389 wx.EVT_MENU(self.__doc_context_menu, ID, self.__access_external_original) 1390 1391 ID = wx.NewId() 1392 self.__doc_context_menu.Append(ID, _('Edit corresponding encounter')) 1393 wx.EVT_MENU(self.__doc_context_menu, ID, self.__edit_encounter_details) 1394 1395 ID = wx.NewId() 1396 self.__doc_context_menu.Append(ID, _('Select corresponding encounter')) 1397 wx.EVT_MENU(self.__doc_context_menu, ID, self.__select_encounter) 1398 1399 # self.__doc_context_menu.AppendSeparator() 1400 1401 ID = wx.NewId() 1402 self.__doc_context_menu.Append(ID, _('Manage descriptions')) 1403 wx.EVT_MENU(self.__doc_context_menu, ID, self.__manage_document_descriptions)
1404 1405 # document / description 1406 # self.__desc_menu = wx.Menu() 1407 # ID = wx.NewId() 1408 # self.__doc_context_menu.AppendMenu(ID, _('Descriptions ...'), self.__desc_menu) 1409 1410 # ID = wx.NewId() 1411 # self.__desc_menu.Append(ID, _('Add new description')) 1412 # wx.EVT_MENU(self.__desc_menu, ID, self.__add_doc_desc) 1413 1414 # ID = wx.NewId() 1415 # self.__desc_menu.Append(ID, _('Delete description')) 1416 # wx.EVT_MENU(self.__desc_menu, ID, self.__del_doc_desc) 1417 1418 # self.__desc_menu.AppendSeparator() 1419 #--------------------------------------------------------
1420 - def __populate_tree(self):
1421 1422 wx.BeginBusyCursor() 1423 1424 # clean old tree 1425 if self.root is not None: 1426 self.DeleteAllItems() 1427 1428 # init new tree 1429 self.root = self.AddRoot(cDocTree._root_node_labels[self.__sort_mode], -1, -1) 1430 self.SetItemPyData(self.root, None) 1431 self.SetItemHasChildren(self.root, False) 1432 1433 # read documents from database 1434 curr_pat = gmPerson.gmCurrentPatient() 1435 docs_folder = curr_pat.get_document_folder() 1436 docs = docs_folder.get_documents() 1437 1438 if docs is None: 1439 gmGuiHelpers.gm_show_error ( 1440 aMessage = _('Error searching documents.'), 1441 aTitle = _('loading document list') 1442 ) 1443 # avoid recursion of GUI updating 1444 wx.EndBusyCursor() 1445 return True 1446 1447 if len(docs) == 0: 1448 wx.EndBusyCursor() 1449 return True 1450 1451 # fill new tree from document list 1452 self.SetItemHasChildren(self.root, True) 1453 1454 # add our documents as first level nodes 1455 intermediate_nodes = {} 1456 for doc in docs: 1457 1458 parts = doc.parts 1459 1460 label = _('%s%7s %s:%s (%s part(s)%s)') % ( 1461 gmTools.bool2subst(doc.has_unreviewed_parts, gmTools.u_writing_hand, u'', u'?'), 1462 doc['clin_when'].strftime('%m/%Y'), 1463 doc['l10n_type'][:26], 1464 gmTools.coalesce(initial = doc['comment'], instead = u'', template_initial = u' %s'), 1465 len(parts), 1466 gmTools.coalesce(initial = doc['ext_ref'], instead = u'', template_initial = u', \u00BB%s\u00AB') 1467 ) 1468 1469 # need intermediate branch level ? 1470 if self.__sort_mode == 'episode': 1471 lbl = doc['episode'] # it'd be nice to also show the issue but we don't have that 1472 if not intermediate_nodes.has_key(lbl): 1473 intermediate_nodes[lbl] = self.AppendItem(parent = self.root, text = lbl) 1474 self.SetItemBold(intermediate_nodes[lbl], bold = True) 1475 self.SetItemPyData(intermediate_nodes[lbl], None) 1476 self.SetItemHasChildren(intermediate_nodes[lbl], True) 1477 parent = intermediate_nodes[lbl] 1478 elif self.__sort_mode == 'type': 1479 if not intermediate_nodes.has_key(doc['l10n_type']): 1480 intermediate_nodes[doc['l10n_type']] = self.AppendItem(parent = self.root, text = doc['l10n_type']) 1481 self.SetItemBold(intermediate_nodes[doc['l10n_type']], bold = True) 1482 self.SetItemPyData(intermediate_nodes[doc['l10n_type']], None) 1483 self.SetItemHasChildren(intermediate_nodes[doc['l10n_type']], True) 1484 parent = intermediate_nodes[doc['l10n_type']] 1485 else: 1486 parent = self.root 1487 1488 doc_node = self.AppendItem(parent = parent, text = label) 1489 #self.SetItemBold(doc_node, bold = True) 1490 self.SetItemPyData(doc_node, doc) 1491 if len(parts) == 0: 1492 self.SetItemHasChildren(doc_node, False) 1493 else: 1494 self.SetItemHasChildren(doc_node, True) 1495 1496 # now add parts as child nodes 1497 for part in parts: 1498 # if part['clinically_relevant']: 1499 # rel = ' [%s]' % _('Cave') 1500 # else: 1501 # rel = '' 1502 f_ext = u'' 1503 if part['filename'] is not None: 1504 f_ext = os.path.splitext(part['filename'])[1].strip('.').strip() 1505 if f_ext != u'': 1506 f_ext = u' .' + f_ext.upper() 1507 label = '%s%s (%s%s)%s' % ( 1508 gmTools.bool2str ( 1509 boolean = part['reviewed'] or part['reviewed_by_you'] or part['reviewed_by_intended_reviewer'], 1510 true_str = u'', 1511 false_str = gmTools.u_writing_hand 1512 ), 1513 _('part %2s') % part['seq_idx'], 1514 gmTools.size2str(part['size']), 1515 f_ext, 1516 gmTools.coalesce ( 1517 part['obj_comment'], 1518 u'', 1519 u': %s%%s%s' % (gmTools.u_left_double_angle_quote, gmTools.u_right_double_angle_quote) 1520 ) 1521 ) 1522 1523 part_node = self.AppendItem(parent = doc_node, text = label) 1524 self.SetItemPyData(part_node, part) 1525 self.SetItemHasChildren(part_node, False) 1526 1527 self.__sort_nodes() 1528 self.SelectItem(self.root) 1529 1530 # FIXME: apply expansion state if available or else ... 1531 # FIXME: ... uncollapse to default state 1532 self.Expand(self.root) 1533 if self.__sort_mode in ['episode', 'type']: 1534 for key in intermediate_nodes.keys(): 1535 self.Expand(intermediate_nodes[key]) 1536 1537 wx.EndBusyCursor() 1538 1539 return True
1540 #------------------------------------------------------------------------
1541 - def OnCompareItems (self, node1=None, node2=None):
1542 """Used in sorting items. 1543 1544 -1: 1 < 2 1545 0: 1 = 2 1546 1: 1 > 2 1547 """ 1548 # Windows can send bogus events so ignore that 1549 if not node1: 1550 _log.debug('invalid node 1') 1551 return 0 1552 if not node2: 1553 _log.debug('invalid node 2') 1554 return 0 1555 if not node1.IsOk(): 1556 _log.debug('no data on node 1') 1557 return 0 1558 if not node2.IsOk(): 1559 _log.debug('no data on node 2') 1560 return 0 1561 1562 data1 = self.GetPyData(node1) 1563 data2 = self.GetPyData(node2) 1564 1565 # doc node 1566 if isinstance(data1, gmDocuments.cDocument): 1567 1568 date_field = 'clin_when' 1569 #date_field = 'modified_when' 1570 1571 if self.__sort_mode == 'age': 1572 # reverse sort by date 1573 if data1[date_field] > data2[date_field]: 1574 return -1 1575 if data1[date_field] == data2[date_field]: 1576 return 0 1577 return 1 1578 1579 elif self.__sort_mode == 'episode': 1580 if data1['episode'] < data2['episode']: 1581 return -1 1582 if data1['episode'] == data2['episode']: 1583 # inner sort: reverse by date 1584 if data1[date_field] > data2[date_field]: 1585 return -1 1586 if data1[date_field] == data2[date_field]: 1587 return 0 1588 return 1 1589 return 1 1590 1591 elif self.__sort_mode == 'review': 1592 # equality 1593 if data1.has_unreviewed_parts == data2.has_unreviewed_parts: 1594 # inner sort: reverse by date 1595 if data1[date_field] > data2[date_field]: 1596 return -1 1597 if data1[date_field] == data2[date_field]: 1598 return 0 1599 return 1 1600 if data1.has_unreviewed_parts: 1601 return -1 1602 return 1 1603 1604 elif self.__sort_mode == 'type': 1605 if data1['l10n_type'] < data2['l10n_type']: 1606 return -1 1607 if data1['l10n_type'] == data2['l10n_type']: 1608 # inner sort: reverse by date 1609 if data1[date_field] > data2[date_field]: 1610 return -1 1611 if data1[date_field] == data2[date_field]: 1612 return 0 1613 return 1 1614 return 1 1615 1616 else: 1617 _log.error('unknown document sort mode [%s], reverse-sorting by age', self.__sort_mode) 1618 # reverse sort by date 1619 if data1[date_field] > data2[date_field]: 1620 return -1 1621 if data1[date_field] == data2[date_field]: 1622 return 0 1623 return 1 1624 1625 # part node 1626 if isinstance(data1, gmDocuments.cDocumentPart): 1627 # compare sequence IDs (= "page" numbers) 1628 # FIXME: wrong order ? 1629 if data1['seq_idx'] < data2['seq_idx']: 1630 return -1 1631 if data1['seq_idx'] == data2['seq_idx']: 1632 return 0 1633 return 1 1634 1635 # else sort alphabetically 1636 if None in [data1, data2]: 1637 l1 = self.GetItemText(node1) 1638 l2 = self.GetItemText(node2) 1639 if l1 < l2: 1640 return -1 1641 if l1 == l2: 1642 return 0 1643 else: 1644 if data1 < data2: 1645 return -1 1646 if data1 == data2: 1647 return 0 1648 return 1
1649 #------------------------------------------------------------------------ 1650 # event handlers 1651 #------------------------------------------------------------------------
1652 - def _on_doc_mod_db(self, *args, **kwargs):
1653 # FIXME: remember current expansion state 1654 wx.CallAfter(self._schedule_data_reget)
1655 #------------------------------------------------------------------------
1656 - def _on_doc_page_mod_db(self, *args, **kwargs):
1657 # FIXME: remember current expansion state 1658 wx.CallAfter(self._schedule_data_reget)
1659 #------------------------------------------------------------------------
1660 - def _on_pre_patient_selection(self, *args, **kwargs):
1661 # FIXME: self.__store_expansion_history_in_db 1662 1663 # empty out tree 1664 if self.root is not None: 1665 self.DeleteAllItems() 1666 self.root = None
1667 #------------------------------------------------------------------------
1668 - def _on_post_patient_selection(self, *args, **kwargs):
1669 # FIXME: self.__load_expansion_history_from_db (but not apply it !) 1670 self._schedule_data_reget()
1671 #------------------------------------------------------------------------
1672 - def _on_activate(self, event):
1673 node = event.GetItem() 1674 node_data = self.GetPyData(node) 1675 1676 # exclude pseudo root node 1677 if node_data is None: 1678 return None 1679 1680 # expand/collapse documents on activation 1681 if isinstance(node_data, gmDocuments.cDocument): 1682 self.Toggle(node) 1683 return True 1684 1685 # string nodes are labels such as episodes which may or may not have children 1686 if type(node_data) == type('string'): 1687 self.Toggle(node) 1688 return True 1689 1690 self.__display_part(part = node_data) 1691 return True
1692 #--------------------------------------------------------
1693 - def __on_right_click(self, evt):
1694 1695 node = evt.GetItem() 1696 self.__curr_node_data = self.GetPyData(node) 1697 1698 # exclude pseudo root node 1699 if self.__curr_node_data is None: 1700 return None 1701 1702 # documents 1703 if isinstance(self.__curr_node_data, gmDocuments.cDocument): 1704 self.__handle_doc_context() 1705 1706 # parts 1707 if isinstance(self.__curr_node_data, gmDocuments.cDocumentPart): 1708 self.__handle_part_context() 1709 1710 del self.__curr_node_data 1711 evt.Skip()
1712 #--------------------------------------------------------
1713 - def __activate_as_current_photo(self, evt):
1714 self.__curr_node_data.set_as_active_photograph()
1715 #--------------------------------------------------------
1716 - def __display_curr_part(self, evt):
1717 self.__display_part(part = self.__curr_node_data)
1718 #--------------------------------------------------------
1719 - def __review_curr_part(self, evt):
1720 self.__review_part(part = self.__curr_node_data)
1721 #--------------------------------------------------------
1722 - def __manage_document_descriptions(self, evt):
1723 manage_document_descriptions(parent = self, document = self.__curr_node_data)
1724 #-------------------------------------------------------- 1725 # internal API 1726 #--------------------------------------------------------
1727 - def __sort_nodes(self, start_node=None):
1728 1729 if start_node is None: 1730 start_node = self.GetRootItem() 1731 1732 # protect against empty tree where not even 1733 # a root node exists 1734 if not start_node.IsOk(): 1735 return True 1736 1737 self.SortChildren(start_node) 1738 1739 child_node, cookie = self.GetFirstChild(start_node) 1740 while child_node.IsOk(): 1741 self.__sort_nodes(start_node = child_node) 1742 child_node, cookie = self.GetNextChild(start_node, cookie) 1743 1744 return
1745 #--------------------------------------------------------
1746 - def __handle_doc_context(self):
1747 self.PopupMenu(self.__doc_context_menu, wx.DefaultPosition)
1748 #--------------------------------------------------------
1749 - def __handle_part_context(self):
1750 1751 # make active patient photograph 1752 if self.__curr_node_data['type'] == 'patient photograph': 1753 ID = wx.NewId() 1754 self.__part_context_menu.Append(ID, _('Activate as current photo')) 1755 wx.EVT_MENU(self.__part_context_menu, ID, self.__activate_as_current_photo) 1756 else: 1757 ID = None 1758 1759 self.PopupMenu(self.__part_context_menu, wx.DefaultPosition) 1760 1761 if ID is not None: 1762 self.__part_context_menu.Delete(ID)
1763 #-------------------------------------------------------- 1764 # part level context menu handlers 1765 #--------------------------------------------------------
1766 - def __display_part(self, part):
1767 """Display document part.""" 1768 1769 # sanity check 1770 if part['size'] == 0: 1771 _log.debug('cannot display part [%s] - 0 bytes', part['pk_obj']) 1772 gmGuiHelpers.gm_show_error ( 1773 aMessage = _('Document part does not seem to exist in database !'), 1774 aTitle = _('showing document') 1775 ) 1776 return None 1777 1778 wx.BeginBusyCursor() 1779 1780 cfg = gmCfg.cCfgSQL() 1781 1782 # # get export directory for temporary files 1783 # tmp_dir = gmTools.coalesce ( 1784 # cfg.get2 ( 1785 # option = "horstspace.tmp_dir", 1786 # workplace = gmSurgery.gmCurrentPractice().active_workplace, 1787 # bias = 'workplace' 1788 # ), 1789 # os.path.expanduser(os.path.join('~', '.gnumed', 'tmp')) 1790 # ) 1791 # _log.debug("temporary directory [%s]", tmp_dir) 1792 1793 # determine database export chunk size 1794 chunksize = int( 1795 cfg.get2 ( 1796 option = "horstspace.blob_export_chunk_size", 1797 workplace = gmSurgery.gmCurrentPractice().active_workplace, 1798 bias = 'workplace', 1799 default = default_chunksize 1800 )) 1801 1802 # shall we force blocking during view ? 1803 block_during_view = bool( cfg.get2 ( 1804 option = 'horstspace.document_viewer.block_during_view', 1805 workplace = gmSurgery.gmCurrentPractice().active_workplace, 1806 bias = 'user', 1807 default = None 1808 )) 1809 1810 # display it 1811 successful, msg = part.display_via_mime ( 1812 # tmpdir = tmp_dir, 1813 chunksize = chunksize, 1814 block = block_during_view 1815 ) 1816 1817 wx.EndBusyCursor() 1818 1819 if not successful: 1820 gmGuiHelpers.gm_show_error ( 1821 aMessage = _('Cannot display document part:\n%s') % msg, 1822 aTitle = _('showing document') 1823 ) 1824 return None 1825 1826 # handle review after display 1827 # 0: never 1828 # 1: always 1829 # 2: if no review by myself exists yet 1830 # 3: if no review at all exists yet 1831 # 4: if no review by responsible reviewer 1832 review_after_display = int(cfg.get2 ( 1833 option = 'horstspace.document_viewer.review_after_display', 1834 workplace = gmSurgery.gmCurrentPractice().active_workplace, 1835 bias = 'user', 1836 default = 3 1837 )) 1838 if review_after_display == 1: # always review 1839 self.__review_part(part=part) 1840 elif review_after_display == 2: # review if no review by me exists 1841 review_by_me = filter(lambda rev: rev['is_your_review'], part.get_reviews()) 1842 if len(review_by_me) == 0: 1843 self.__review_part(part = part) 1844 elif review_after_display == 3: 1845 if len(part.get_reviews()) == 0: 1846 self.__review_part(part = part) 1847 elif review_after_display == 4: 1848 reviewed_by_responsible = filter(lambda rev: rev['is_review_by_responsible_reviewer'], part.get_reviews()) 1849 if len(reviewed_by_responsible) == 0: 1850 self.__review_part(part = part) 1851 1852 return True
1853 #--------------------------------------------------------
1854 - def __review_part(self, part=None):
1855 dlg = cReviewDocPartDlg ( 1856 parent = self, 1857 id = -1, 1858 part = part 1859 ) 1860 dlg.ShowModal() 1861 dlg.Destroy()
1862 #--------------------------------------------------------
1863 - def __process_part(self, action=None, l10n_action=None):
1864 1865 gmHooks.run_hook_script(hook = u'before_%s_doc_part' % action) 1866 1867 wx.BeginBusyCursor() 1868 1869 # detect wrapper 1870 found, external_cmd = gmShellAPI.detect_external_binary(u'gm-%s_doc' % action) 1871 if not found: 1872 found, external_cmd = gmShellAPI.detect_external_binary(u'gm-%s_doc.bat' % action) 1873 if not found: 1874 _log.error('neither of gm-%s_doc or gm-%s_doc.bat found', action, action) 1875 wx.EndBusyCursor() 1876 gmGuiHelpers.gm_show_error ( 1877 _('Cannot %(l10n_action)s document part - %(l10n_action)s command not found.\n' 1878 '\n' 1879 'Either of gm_%(action)s_doc.sh or gm_%(action)s_doc.bat\n' 1880 'must be in the execution path. The command will\n' 1881 'be passed the filename to %(l10n_action)s.' 1882 ) % {'action': action, 'l10n_action': l10n_action}, 1883 _('Processing document part: %s') % l10n_action 1884 ) 1885 return 1886 1887 cfg = gmCfg.cCfgSQL() 1888 1889 # # get export directory for temporary files 1890 # tmp_dir = gmTools.coalesce ( 1891 # cfg.get2 ( 1892 # option = "horstspace.tmp_dir", 1893 # workplace = gmSurgery.gmCurrentPractice().active_workplace, 1894 # bias = 'workplace' 1895 # ), 1896 # os.path.expanduser(os.path.join('~', '.gnumed', 'tmp')) 1897 # ) 1898 # _log.debug("temporary directory [%s]", tmp_dir) 1899 1900 # determine database export chunk size 1901 chunksize = int(cfg.get2 ( 1902 option = "horstspace.blob_export_chunk_size", 1903 workplace = gmSurgery.gmCurrentPractice().active_workplace, 1904 bias = 'workplace', 1905 default = default_chunksize 1906 )) 1907 1908 part_file = self.__curr_node_data.export_to_file ( 1909 # aTempDir = tmp_dir, 1910 aChunkSize = chunksize 1911 ) 1912 1913 cmd = u'%s %s' % (external_cmd, part_file) 1914 success = gmShellAPI.run_command_in_shell ( 1915 command = cmd, 1916 blocking = False 1917 ) 1918 1919 wx.EndBusyCursor() 1920 1921 if not success: 1922 _log.error('%s command failed: [%s]', action, cmd) 1923 gmGuiHelpers.gm_show_error ( 1924 _('Cannot %(l10n_action)s document part - %(l10n_action)s command failed.\n' 1925 '\n' 1926 'You may need to check and fix either of\n' 1927 ' gm_%(action)s_doc.sh (Unix/Mac) or\n' 1928 ' gm_%(action)s_doc.bat (Windows)\n' 1929 '\n' 1930 'The command is passed the filename to %(l10n_action)s.' 1931 ) % {'action': action, 'l10n_action': l10n_action}, 1932 _('Processing document part: %s') % l10n_action 1933 )
1934 #-------------------------------------------------------- 1935 # FIXME: icons in the plugin toolbar
1936 - def __print_part(self, evt):
1937 self.__process_part(action = u'print', l10n_action = _('print'))
1938 #--------------------------------------------------------
1939 - def __fax_part(self, evt):
1940 self.__process_part(action = u'fax', l10n_action = _('fax'))
1941 #--------------------------------------------------------
1942 - def __mail_part(self, evt):
1943 self.__process_part(action = u'mail', l10n_action = _('mail'))
1944 #-------------------------------------------------------- 1945 # document level context menu handlers 1946 #--------------------------------------------------------
1947 - def __select_encounter(self, evt):
1948 enc = gmEMRStructWidgets.select_encounters ( 1949 parent = self, 1950 patient = gmPerson.gmCurrentPatient() 1951 ) 1952 if not enc: 1953 return 1954 self.__curr_node_data['pk_encounter'] = enc['pk_encounter'] 1955 self.__curr_node_data.save()
1956 #--------------------------------------------------------
1957 - def __edit_encounter_details(self, evt):
1958 enc = gmEMRStructItems.cEncounter(aPK_obj = self.__curr_node_data['pk_encounter']) 1959 gmEMRStructWidgets.edit_encounter(parent = self, encounter = enc)
1960 #--------------------------------------------------------
1961 - def __process_doc(self, action=None, l10n_action=None):
1962 1963 gmHooks.run_hook_script(hook = u'before_%s_doc' % action) 1964 1965 wx.BeginBusyCursor() 1966 1967 # detect wrapper 1968 found, external_cmd = gmShellAPI.detect_external_binary(u'gm-%s_doc' % action) 1969 if not found: 1970 found, external_cmd = gmShellAPI.detect_external_binary(u'gm-%s_doc.bat' % action) 1971 if not found: 1972 _log.error('neither of gm-%s_doc or gm-%s_doc.bat found', action, action) 1973 wx.EndBusyCursor() 1974 gmGuiHelpers.gm_show_error ( 1975 _('Cannot %(l10n_action)s document - %(l10n_action)s command not found.\n' 1976 '\n' 1977 'Either of gm_%(action)s_doc.sh or gm_%(action)s_doc.bat\n' 1978 'must be in the execution path. The command will\n' 1979 'be passed a list of filenames to %(l10n_action)s.' 1980 ) % {'action': action, 'l10n_action': l10n_action}, 1981 _('Processing document: %s') % l10n_action 1982 ) 1983 return 1984 1985 cfg = gmCfg.cCfgSQL() 1986 1987 # # get export directory for temporary files 1988 # tmp_dir = gmTools.coalesce ( 1989 # cfg.get2 ( 1990 # option = "horstspace.tmp_dir", 1991 # workplace = gmSurgery.gmCurrentPractice().active_workplace, 1992 # bias = 'workplace' 1993 # ), 1994 # os.path.expanduser(os.path.join('~', '.gnumed', 'tmp')) 1995 # ) 1996 # _log.debug("temporary directory [%s]", tmp_dir) 1997 1998 # determine database export chunk size 1999 chunksize = int(cfg.get2 ( 2000 option = "horstspace.blob_export_chunk_size", 2001 workplace = gmSurgery.gmCurrentPractice().active_workplace, 2002 bias = 'workplace', 2003 default = default_chunksize 2004 )) 2005 2006 part_files = self.__curr_node_data.export_parts_to_files ( 2007 # export_dir = tmp_dir, 2008 chunksize = chunksize 2009 ) 2010 2011 cmd = external_cmd + u' ' + u' '.join(part_files) 2012 success = gmShellAPI.run_command_in_shell ( 2013 command = cmd, 2014 blocking = False 2015 ) 2016 2017 wx.EndBusyCursor() 2018 2019 if not success: 2020 _log.error('%s command failed: [%s]', action, cmd) 2021 gmGuiHelpers.gm_show_error ( 2022 _('Cannot %(l10n_action)s document - %(l10n_action)s command failed.\n' 2023 '\n' 2024 'You may need to check and fix either of\n' 2025 ' gm_%(action)s_doc.sh (Unix/Mac) or\n' 2026 ' gm_%(action)s_doc.bat (Windows)\n' 2027 '\n' 2028 'The command is passed a list of filenames to %(l10n_action)s.' 2029 ) % {'action': action, 'l10n_action': l10n_action}, 2030 _('Processing document: %s') % l10n_action 2031 )
2032 #-------------------------------------------------------- 2033 # FIXME: icons in the plugin toolbar
2034 - def __print_doc(self, evt):
2035 self.__process_doc(action = u'print', l10n_action = _('print'))
2036 #--------------------------------------------------------
2037 - def __fax_doc(self, evt):
2038 self.__process_doc(action = u'fax', l10n_action = _('fax'))
2039 #--------------------------------------------------------
2040 - def __mail_doc(self, evt):
2041 self.__process_doc(action = u'mail', l10n_action = _('mail'))
2042 #--------------------------------------------------------
2043 - def __access_external_original(self, evt):
2044 2045 gmHooks.run_hook_script(hook = u'before_external_doc_access') 2046 2047 wx.BeginBusyCursor() 2048 2049 # detect wrapper 2050 found, external_cmd = gmShellAPI.detect_external_binary(u'gm_access_external_doc.sh') 2051 if not found: 2052 found, external_cmd = gmShellAPI.detect_external_binary(u'gm_access_external_doc.bat') 2053 if not found: 2054 _log.error('neither of gm_access_external_doc.sh or .bat found') 2055 wx.EndBusyCursor() 2056 gmGuiHelpers.gm_show_error ( 2057 _('Cannot access external document - access command not found.\n' 2058 '\n' 2059 'Either of gm_access_external_doc.sh or *.bat must be\n' 2060 'in the execution path. The command will be passed the\n' 2061 'document type and the reference URL for processing.' 2062 ), 2063 _('Accessing external document') 2064 ) 2065 return 2066 2067 cmd = u'%s "%s" "%s"' % (external_cmd, self.__curr_node_data['type'], self.__curr_node_data['ext_ref']) 2068 success = gmShellAPI.run_command_in_shell ( 2069 command = cmd, 2070 blocking = False 2071 ) 2072 2073 wx.EndBusyCursor() 2074 2075 if not success: 2076 _log.error('External access command failed: [%s]', cmd) 2077 gmGuiHelpers.gm_show_error ( 2078 _('Cannot access external document - access command failed.\n' 2079 '\n' 2080 'You may need to check and fix either of\n' 2081 ' gm_access_external_doc.sh (Unix/Mac) or\n' 2082 ' gm_access_external_doc.bat (Windows)\n' 2083 '\n' 2084 'The command is passed the document type and the\n' 2085 'external reference URL on the command line.' 2086 ), 2087 _('Accessing external document') 2088 )
2089 #--------------------------------------------------------
2090 - def __export_doc_to_disk(self, evt):
2091 """Export document into directory. 2092 2093 - one file per object 2094 - into subdirectory named after patient 2095 """ 2096 pat = gmPerson.gmCurrentPatient() 2097 dname = '%s-%s%s' % ( 2098 self.__curr_node_data['l10n_type'], 2099 self.__curr_node_data['clin_when'].strftime('%Y-%m-%d'), 2100 gmTools.coalesce(self.__curr_node_data['ext_ref'], '', '-%s').replace(' ', '_') 2101 ) 2102 def_dir = os.path.expanduser(os.path.join('~', 'gnumed', 'export', 'docs', pat['dirname'], dname)) 2103 gmTools.mkdir(def_dir) 2104 2105 dlg = wx.DirDialog ( 2106 parent = self, 2107 message = _('Save document into directory ...'), 2108 defaultPath = def_dir, 2109 style = wx.DD_DEFAULT_STYLE 2110 ) 2111 result = dlg.ShowModal() 2112 dirname = dlg.GetPath() 2113 dlg.Destroy() 2114 2115 if result != wx.ID_OK: 2116 return True 2117 2118 wx.BeginBusyCursor() 2119 2120 cfg = gmCfg.cCfgSQL() 2121 2122 # determine database export chunk size 2123 chunksize = int(cfg.get2 ( 2124 option = "horstspace.blob_export_chunk_size", 2125 workplace = gmSurgery.gmCurrentPractice().active_workplace, 2126 bias = 'workplace', 2127 default = default_chunksize 2128 )) 2129 2130 fnames = self.__curr_node_data.export_parts_to_files(export_dir = dirname, chunksize = chunksize) 2131 2132 wx.EndBusyCursor() 2133 2134 gmDispatcher.send(signal='statustext', msg=_('Successfully exported %s parts into the directory [%s].') % (len(fnames), dirname)) 2135 2136 return True
2137 #--------------------------------------------------------
2138 - def __delete_document(self, evt):
2139 result = gmGuiHelpers.gm_show_question ( 2140 aMessage = _('Are you sure you want to delete the document ?'), 2141 aTitle = _('Deleting document') 2142 ) 2143 if result is True: 2144 curr_pat = gmPerson.gmCurrentPatient() 2145 emr = curr_pat.get_emr() 2146 enc = emr.active_encounter 2147 gmDocuments.delete_document(document_id = self.__curr_node_data['pk_doc'], encounter_id = enc['pk_encounter'])
2148 #============================================================ 2149 # main 2150 #------------------------------------------------------------ 2151 if __name__ == '__main__': 2152 2153 gmI18N.activate_locale() 2154 gmI18N.install_domain(domain = 'gnumed') 2155 2156 #---------------------------------------- 2157 #---------------------------------------- 2158 if (len(sys.argv) > 1) and (sys.argv[1] == 'test'): 2159 # test_*() 2160 pass 2161 2162 #============================================================ 2163