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