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