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