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