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