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 tmpdir = os.path.expanduser(os.path.join('~', '.gnumed', 'tmp')) 981 try: 982 gmTools.mkdir(tmpdir) 983 except: 984 tmpdir = None 985 986 # FIXME: configure whether to use XSane or sane directly 987 # FIXME: add support for xsane_device_settings argument 988 try: 989 fnames = self.scan_module.acquire_pages_into_files ( 990 device = chosen_device, 991 delay = 5, 992 tmpdir = tmpdir, 993 calling_window = self 994 ) 995 except OSError: 996 _log.exception('problem acquiring image from source') 997 gmGuiHelpers.gm_show_error ( 998 aMessage = _( 999 'No pages could be acquired from the source.\n\n' 1000 'This may mean the scanner driver is not properly installed.\n\n' 1001 'On Windows you must install the TWAIN Python module\n' 1002 'while on Linux and MacOSX it is recommended to install\n' 1003 'the XSane package.' 1004 ), 1005 aTitle = _('acquiring page') 1006 ) 1007 return None 1008 1009 if len(fnames) == 0: # no pages scanned 1010 return True 1011 1012 self.acquired_pages.extend(fnames) 1013 self.__reload_LBOX_doc_pages() 1014 1015 return True
1016 #--------------------------------------------------------
1017 - def _load_btn_pressed(self, evt):
1018 # patient file chooser 1019 dlg = wx.FileDialog ( 1020 parent = None, 1021 message = _('Choose a file'), 1022 defaultDir = os.path.expanduser(os.path.join('~', 'gnumed')), 1023 defaultFile = '', 1024 wildcard = "%s (*)|*|TIFFs (*.tif)|*.tif|JPEGs (*.jpg)|*.jpg|%s (*.*)|*.*" % (_('all files'), _('all files (Win)')), 1025 style = wx.OPEN | wx.HIDE_READONLY | wx.FILE_MUST_EXIST | wx.MULTIPLE 1026 ) 1027 result = dlg.ShowModal() 1028 if result != wx.ID_CANCEL: 1029 files = dlg.GetPaths() 1030 for file in files: 1031 self.acquired_pages.append(file) 1032 self.__reload_LBOX_doc_pages() 1033 dlg.Destroy()
1034 #--------------------------------------------------------
1035 - def _show_btn_pressed(self, evt):
1036 # did user select a page ? 1037 page_idx = self._LBOX_doc_pages.GetSelection() 1038 if page_idx == -1: 1039 gmGuiHelpers.gm_show_info ( 1040 aMessage = _('You must select a part before you can view it.'), 1041 aTitle = _('displaying part') 1042 ) 1043 return None 1044 # now, which file was that again ? 1045 page_fname = self._LBOX_doc_pages.GetClientData(page_idx) 1046 1047 (result, msg) = gmMimeLib.call_viewer_on_file(page_fname) 1048 if not result: 1049 gmGuiHelpers.gm_show_warning ( 1050 aMessage = _('Cannot display document part:\n%s') % msg, 1051 aTitle = _('displaying part') 1052 ) 1053 return None 1054 return 1
1055 #--------------------------------------------------------
1056 - def _del_btn_pressed(self, event):
1057 page_idx = self._LBOX_doc_pages.GetSelection() 1058 if page_idx == -1: 1059 gmGuiHelpers.gm_show_info ( 1060 aMessage = _('You must select a part before you can delete it.'), 1061 aTitle = _('deleting part') 1062 ) 1063 return None 1064 page_fname = self._LBOX_doc_pages.GetClientData(page_idx) 1065 1066 # 1) del item from self.acquired_pages 1067 self.acquired_pages[page_idx:(page_idx+1)] = [] 1068 1069 # 2) reload list box 1070 self.__reload_LBOX_doc_pages() 1071 1072 # 3) optionally kill file in the file system 1073 do_delete = gmGuiHelpers.gm_show_question ( 1074 _('The part has successfully been removed from the document.\n' 1075 '\n' 1076 'Do you also want to permanently delete the file\n' 1077 '\n' 1078 ' [%s]\n' 1079 '\n' 1080 'from which this document part was loaded ?\n' 1081 '\n' 1082 'If it is a temporary file for a page you just scanned\n' 1083 'this makes a lot of sense. In other cases you may not\n' 1084 'want to lose the file.\n' 1085 '\n' 1086 'Pressing [YES] will permanently remove the file\n' 1087 'from your computer.\n' 1088 ) % page_fname, 1089 _('Removing document part') 1090 ) 1091 if do_delete: 1092 try: 1093 os.remove(page_fname) 1094 except: 1095 _log.exception('Error deleting file.') 1096 gmGuiHelpers.gm_show_error ( 1097 aMessage = _('Cannot delete part in file [%s].\n\nYou may not have write access to it.') % page_fname, 1098 aTitle = _('deleting part') 1099 ) 1100 1101 return 1
1102 #--------------------------------------------------------
1103 - def _save_btn_pressed(self, evt):
1104 1105 if not self.__valid_for_save(): 1106 return False 1107 1108 wx.BeginBusyCursor() 1109 1110 pat = gmPerson.gmCurrentPatient() 1111 doc_folder = pat.get_document_folder() 1112 emr = pat.get_emr() 1113 1114 # create new document 1115 pk_episode = self._PhWheel_episode.GetData() 1116 if pk_episode is None: 1117 episode = emr.add_episode ( 1118 episode_name = self._PhWheel_episode.GetValue().strip(), 1119 is_open = True 1120 ) 1121 if episode is None: 1122 wx.EndBusyCursor() 1123 gmGuiHelpers.gm_show_error ( 1124 aMessage = _('Cannot start episode [%s].') % self._PhWheel_episode.GetValue().strip(), 1125 aTitle = _('saving document') 1126 ) 1127 return False 1128 pk_episode = episode['pk_episode'] 1129 1130 encounter = emr.active_encounter['pk_encounter'] 1131 document_type = self._PhWheel_doc_type.GetData() 1132 new_doc = doc_folder.add_document(document_type, encounter, pk_episode) 1133 if new_doc is None: 1134 wx.EndBusyCursor() 1135 gmGuiHelpers.gm_show_error ( 1136 aMessage = _('Cannot create new document.'), 1137 aTitle = _('saving document') 1138 ) 1139 return False 1140 1141 # update business object with metadata 1142 # - date of generation 1143 new_doc['clin_when'] = self._PhWheel_doc_date.GetData().get_pydt() 1144 # - external reference 1145 cfg = gmCfg.cCfgSQL() 1146 generate_uuid = bool ( 1147 cfg.get2 ( 1148 option = 'horstspace.scan_index.generate_doc_uuid', 1149 workplace = gmSurgery.gmCurrentPractice().active_workplace, 1150 bias = 'user', 1151 default = False 1152 ) 1153 ) 1154 ref = None 1155 if generate_uuid: 1156 ref = gmDocuments.get_ext_ref() 1157 if ref is not None: 1158 new_doc['ext_ref'] = ref 1159 # - comment 1160 comment = self._PRW_doc_comment.GetLineText(0).strip() 1161 if comment != u'': 1162 new_doc['comment'] = comment 1163 # - save it 1164 if not new_doc.save_payload(): 1165 wx.EndBusyCursor() 1166 gmGuiHelpers.gm_show_error ( 1167 aMessage = _('Cannot update document metadata.'), 1168 aTitle = _('saving document') 1169 ) 1170 return False 1171 # - long description 1172 description = self._TBOX_description.GetValue().strip() 1173 if description != '': 1174 if not new_doc.add_description(description): 1175 wx.EndBusyCursor() 1176 gmGuiHelpers.gm_show_error ( 1177 aMessage = _('Cannot add document description.'), 1178 aTitle = _('saving document') 1179 ) 1180 return False 1181 1182 # add document parts from files 1183 success, msg, filename = new_doc.add_parts_from_files ( 1184 files = self.acquired_pages, 1185 reviewer = self._PhWheel_reviewer.GetData() 1186 ) 1187 if not success: 1188 wx.EndBusyCursor() 1189 gmGuiHelpers.gm_show_error ( 1190 aMessage = msg, 1191 aTitle = _('saving document') 1192 ) 1193 return False 1194 1195 # set reviewed status 1196 if self._ChBOX_reviewed.GetValue(): 1197 if not new_doc.set_reviewed ( 1198 technically_abnormal = self._ChBOX_abnormal.GetValue(), 1199 clinically_relevant = self._ChBOX_relevant.GetValue() 1200 ): 1201 msg = _('Error setting "reviewed" status of new document.') 1202 1203 gmHooks.run_hook_script(hook = u'after_new_doc_created') 1204 1205 # inform user 1206 show_id = bool ( 1207 cfg.get2 ( 1208 option = 'horstspace.scan_index.show_doc_id', 1209 workplace = gmSurgery.gmCurrentPractice().active_workplace, 1210 bias = 'user' 1211 ) 1212 ) 1213 wx.EndBusyCursor() 1214 if show_id: 1215 if ref is None: 1216 msg = _('Successfully saved the new document.') 1217 else: 1218 msg = _( 1219 """The reference ID for the new document is: 1220 1221 <%s> 1222 1223 You probably want to write it down on the 1224 original documents. 1225 1226 If you don't care about the ID you can switch 1227 off this message in the GNUmed configuration.""") % ref 1228 gmGuiHelpers.gm_show_info ( 1229 aMessage = msg, 1230 aTitle = _('Saving document') 1231 ) 1232 else: 1233 gmDispatcher.send(signal='statustext', msg=_('Successfully saved new document.')) 1234 1235 self.__init_ui_data() 1236 return True
1237 #--------------------------------------------------------
1238 - def _startover_btn_pressed(self, evt):
1239 self.__init_ui_data()
1240 #--------------------------------------------------------
1241 - def _reviewed_box_checked(self, evt):
1242 self._ChBOX_abnormal.Enable(enable = self._ChBOX_reviewed.GetValue()) 1243 self._ChBOX_relevant.Enable(enable = self._ChBOX_reviewed.GetValue())
1244 #--------------------------------------------------------
1245 - def _on_doc_type_loses_focus(self):
1246 pk_doc_type = self._PhWheel_doc_type.GetData() 1247 if pk_doc_type is None: 1248 self._PRW_doc_comment.unset_context(context = 'pk_doc_type') 1249 else: 1250 self._PRW_doc_comment.set_context(context = 'pk_doc_type', val = pk_doc_type) 1251 return True
1252 #============================================================
1253 -def display_document_part(parent=None, part=None):
1254 1255 if parent is None: 1256 parent = wx.GetApp().GetTopWindow() 1257 1258 # sanity check 1259 if part['size'] == 0: 1260 _log.debug('cannot display part [%s] - 0 bytes', part['pk_obj']) 1261 gmGuiHelpers.gm_show_error ( 1262 aMessage = _('Document part does not seem to exist in database !'), 1263 aTitle = _('showing document') 1264 ) 1265 return None 1266 1267 wx.BeginBusyCursor() 1268 cfg = gmCfg.cCfgSQL() 1269 1270 # determine database export chunk size 1271 chunksize = int( 1272 cfg.get2 ( 1273 option = "horstspace.blob_export_chunk_size", 1274 workplace = gmSurgery.gmCurrentPractice().active_workplace, 1275 bias = 'workplace', 1276 default = 2048 1277 )) 1278 1279 # shall we force blocking during view ? 1280 block_during_view = bool( cfg.get2 ( 1281 option = 'horstspace.document_viewer.block_during_view', 1282 workplace = gmSurgery.gmCurrentPractice().active_workplace, 1283 bias = 'user', 1284 default = None 1285 )) 1286 1287 wx.EndBusyCursor() 1288 1289 # display it 1290 successful, msg = part.display_via_mime ( 1291 chunksize = chunksize, 1292 block = block_during_view 1293 ) 1294 if not successful: 1295 gmGuiHelpers.gm_show_error ( 1296 aMessage = _('Cannot display document part:\n%s') % msg, 1297 aTitle = _('showing document') 1298 ) 1299 return None 1300 1301 # handle review after display 1302 # 0: never 1303 # 1: always 1304 # 2: if no review by myself exists yet 1305 # 3: if no review at all exists yet 1306 # 4: if no review by responsible reviewer 1307 review_after_display = int(cfg.get2 ( 1308 option = 'horstspace.document_viewer.review_after_display', 1309 workplace = gmSurgery.gmCurrentPractice().active_workplace, 1310 bias = 'user', 1311 default = 3 1312 )) 1313 if review_after_display == 1: # always review 1314 review_document_part(parent = parent, part = part) 1315 elif review_after_display == 2: # review if no review by me exists 1316 review_by_me = filter(lambda rev: rev['is_your_review'], part.get_reviews()) 1317 if len(review_by_me) == 0: 1318 review_document_part(parent = parent, part = part) 1319 elif review_after_display == 3: 1320 if len(part.get_reviews()) == 0: 1321 review_document_part(parent = parent, part = part) 1322 elif review_after_display == 4: 1323 reviewed_by_responsible = filter(lambda rev: rev['is_review_by_responsible_reviewer'], part.get_reviews()) 1324 if len(reviewed_by_responsible) == 0: 1325 review_document_part(parent = parent, part = part) 1326 1327 return True
1328 #============================================================
1329 -def manage_documents(parent=None, msg=None):
1330 1331 pat = gmPerson.gmCurrentPatient() 1332 1333 if parent is None: 1334 parent = wx.GetApp().GetTopWindow() 1335 #-------------------------------------------------------- 1336 def edit(document=None): 1337 return
1338 #return edit_consumable_substance(parent = parent, substance = substance, single_entry = (substance is not None)) 1339 #-------------------------------------------------------- 1340 def delete(document): 1341 return 1342 # if substance.is_in_use_by_patients: 1343 # gmDispatcher.send(signal = 'statustext', msg = _('Cannot delete this substance. It is in use.'), beep = True) 1344 # return False 1345 # 1346 # return gmMedication.delete_consumable_substance(substance = substance['pk']) 1347 #------------------------------------------------------------ 1348 def refresh(lctrl): 1349 docs = pat.document_folder.get_documents() 1350 items = [ [ 1351 gmDateTime.pydt_strftime(d['clin_when'], u'%Y-%m-%d', accuracy = gmDateTime.acc_days), 1352 d['l10n_type'], 1353 gmTools.coalesce(d['comment'], u''), 1354 gmTools.coalesce(d['ext_ref'], u''), 1355 d['pk_doc'] 1356 ] for d in docs ] 1357 lctrl.set_string_items(items) 1358 lctrl.set_data(docs) 1359 #------------------------------------------------------------ 1360 if msg is None: 1361 msg = _('Document list for this patient.') 1362 return gmListWidgets.get_choices_from_list ( 1363 parent = parent, 1364 msg = msg, 1365 caption = _('Showing documents.'), 1366 columns = [_('Generated'), _('Type'), _('Comment'), _('Ref #'), u'#'], 1367 single_selection = True, 1368 #new_callback = edit, 1369 #edit_callback = edit, 1370 #delete_callback = delete, 1371 refresh_callback = refresh 1372 #,left_extra_button = (_('Import'), _('Import consumable substances from a drug database.'), add_from_db) 1373 ) 1374 #============================================================ 1375 from Gnumed.wxGladeWidgets import wxgSelectablySortedDocTreePnl 1376
1377 -class cSelectablySortedDocTreePnl(wxgSelectablySortedDocTreePnl.wxgSelectablySortedDocTreePnl):
1378 """A panel with a document tree which can be sorted.""" 1379 #-------------------------------------------------------- 1380 # inherited event handlers 1381 #--------------------------------------------------------
1382 - def _on_sort_by_age_selected(self, evt):
1383 self._doc_tree.sort_mode = 'age' 1384 self._doc_tree.SetFocus() 1385 self._rbtn_sort_by_age.SetValue(True)
1386 #--------------------------------------------------------
1387 - def _on_sort_by_review_selected(self, evt):
1388 self._doc_tree.sort_mode = 'review' 1389 self._doc_tree.SetFocus() 1390 self._rbtn_sort_by_review.SetValue(True)
1391 #--------------------------------------------------------
1392 - def _on_sort_by_episode_selected(self, evt):
1393 self._doc_tree.sort_mode = 'episode' 1394 self._doc_tree.SetFocus() 1395 self._rbtn_sort_by_episode.SetValue(True)
1396 #--------------------------------------------------------
1397 - def _on_sort_by_issue_selected(self, event):
1398 self._doc_tree.sort_mode = 'issue' 1399 self._doc_tree.SetFocus() 1400 self._rbtn_sort_by_issue.SetValue(True)
1401 #--------------------------------------------------------
1402 - def _on_sort_by_type_selected(self, evt):
1403 self._doc_tree.sort_mode = 'type' 1404 self._doc_tree.SetFocus() 1405 self._rbtn_sort_by_type.SetValue(True)
1406 #============================================================
1407 -class cDocTree(wx.TreeCtrl, gmRegetMixin.cRegetOnPaintMixin):
1408 # FIXME: handle expansion state 1409 """This wx.TreeCtrl derivative displays a tree view of stored medical documents. 1410 1411 It listens to document and patient changes and updated itself accordingly. 1412 1413 This acts on the current patient. 1414 """ 1415 _sort_modes = ['age', 'review', 'episode', 'type', 'issue'] 1416 _root_node_labels = None 1417 #--------------------------------------------------------
1418 - def __init__(self, parent, id, *args, **kwds):
1419 """Set up our specialised tree. 1420 """ 1421 kwds['style'] = wx.TR_NO_BUTTONS | wx.NO_BORDER | wx.TR_SINGLE 1422 wx.TreeCtrl.__init__(self, parent, id, *args, **kwds) 1423 1424 gmRegetMixin.cRegetOnPaintMixin.__init__(self) 1425 1426 tmp = _('available documents (%s)') 1427 unsigned = _('unsigned (%s) on top') % u'\u270D' 1428 cDocTree._root_node_labels = { 1429 'age': tmp % _('most recent on top'), 1430 'review': tmp % unsigned, 1431 'episode': tmp % _('sorted by episode'), 1432 'issue': tmp % _('sorted by health issue'), 1433 'type': tmp % _('sorted by type') 1434 } 1435 1436 self.root = None 1437 self.__sort_mode = 'age' 1438 1439 self.__build_context_menus() 1440 self.__register_interests() 1441 self._schedule_data_reget()
1442 #-------------------------------------------------------- 1443 # external API 1444 #--------------------------------------------------------
1445 - def display_selected_part(self, *args, **kwargs):
1446 1447 node = self.GetSelection() 1448 node_data = self.GetPyData(node) 1449 1450 if not isinstance(node_data, gmDocuments.cDocumentPart): 1451 return True 1452 1453 self.__display_part(part = node_data) 1454 return True
1455 #-------------------------------------------------------- 1456 # properties 1457 #--------------------------------------------------------
1458 - def _get_sort_mode(self):
1459 return self.__sort_mode
1460 #-----
1461 - def _set_sort_mode(self, mode):
1462 if mode is None: 1463 mode = 'age' 1464 1465 if mode == self.__sort_mode: 1466 return 1467 1468 if mode not in cDocTree._sort_modes: 1469 raise ValueError('invalid document tree sort mode [%s], valid modes: %s' % (mode, cDocTree._sort_modes)) 1470 1471 self.__sort_mode = mode 1472 1473 curr_pat = gmPerson.gmCurrentPatient() 1474 if not curr_pat.connected: 1475 return 1476 1477 self._schedule_data_reget()
1478 #----- 1479 sort_mode = property(_get_sort_mode, _set_sort_mode) 1480 #-------------------------------------------------------- 1481 # reget-on-paint API 1482 #--------------------------------------------------------
1483 - def _populate_with_data(self):
1484 curr_pat = gmPerson.gmCurrentPatient() 1485 if not curr_pat.connected: 1486 gmDispatcher.send(signal = 'statustext', msg = _('Cannot load documents. No active patient.')) 1487 return False 1488 1489 if not self.__populate_tree(): 1490 return False 1491 1492 return True
1493 #-------------------------------------------------------- 1494 # internal helpers 1495 #--------------------------------------------------------
1496 - def __register_interests(self):
1497 # connect handlers 1498 wx.EVT_TREE_ITEM_ACTIVATED (self, self.GetId(), self._on_activate) 1499 wx.EVT_TREE_ITEM_RIGHT_CLICK (self, self.GetId(), self.__on_right_click) 1500 1501 # wx.EVT_LEFT_DCLICK(self.tree, self.OnLeftDClick) 1502 1503 gmDispatcher.connect(signal = u'pre_patient_selection', receiver = self._on_pre_patient_selection) 1504 gmDispatcher.connect(signal = u'post_patient_selection', receiver = self._on_post_patient_selection) 1505 gmDispatcher.connect(signal = u'doc_mod_db', receiver = self._on_doc_mod_db) 1506 gmDispatcher.connect(signal = u'doc_page_mod_db', receiver = self._on_doc_page_mod_db)
1507 #--------------------------------------------------------
1508 - def __build_context_menus(self):
1509 1510 # --- part context menu --- 1511 self.__part_context_menu = wx.Menu(title = _('Part Actions:')) 1512 1513 ID = wx.NewId() 1514 self.__part_context_menu.Append(ID, _('Display part')) 1515 wx.EVT_MENU(self.__part_context_menu, ID, self.__display_curr_part) 1516 1517 ID = wx.NewId() 1518 self.__part_context_menu.Append(ID, _('%s Sign/Edit properties') % u'\u270D') 1519 wx.EVT_MENU(self.__part_context_menu, ID, self.__review_curr_part) 1520 1521 self.__part_context_menu.AppendSeparator() 1522 1523 item = self.__part_context_menu.Append(-1, _('Delete part')) 1524 self.Bind(wx.EVT_MENU, self.__delete_part, item) 1525 1526 item = self.__part_context_menu.Append(-1, _('Move part')) 1527 self.Bind(wx.EVT_MENU, self.__move_part, item) 1528 1529 ID = wx.NewId() 1530 self.__part_context_menu.Append(ID, _('Print part')) 1531 wx.EVT_MENU(self.__part_context_menu, ID, self.__print_part) 1532 1533 ID = wx.NewId() 1534 self.__part_context_menu.Append(ID, _('Fax part')) 1535 wx.EVT_MENU(self.__part_context_menu, ID, self.__fax_part) 1536 1537 ID = wx.NewId() 1538 self.__part_context_menu.Append(ID, _('Mail part')) 1539 wx.EVT_MENU(self.__part_context_menu, ID, self.__mail_part) 1540 1541 self.__part_context_menu.AppendSeparator() # so we can append some items 1542 1543 # --- doc context menu --- 1544 self.__doc_context_menu = wx.Menu(title = _('Document Actions:')) 1545 1546 ID = wx.NewId() 1547 self.__doc_context_menu.Append(ID, _('%s Sign/Edit properties') % u'\u270D') 1548 wx.EVT_MENU(self.__doc_context_menu, ID, self.__review_curr_part) 1549 1550 self.__doc_context_menu.AppendSeparator() 1551 1552 item = self.__doc_context_menu.Append(-1, _('Add parts')) 1553 self.Bind(wx.EVT_MENU, self.__add_part, item) 1554 1555 ID = wx.NewId() 1556 self.__doc_context_menu.Append(ID, _('Print all parts')) 1557 wx.EVT_MENU(self.__doc_context_menu, ID, self.__print_doc) 1558 1559 ID = wx.NewId() 1560 self.__doc_context_menu.Append(ID, _('Fax all parts')) 1561 wx.EVT_MENU(self.__doc_context_menu, ID, self.__fax_doc) 1562 1563 ID = wx.NewId() 1564 self.__doc_context_menu.Append(ID, _('Mail all parts')) 1565 wx.EVT_MENU(self.__doc_context_menu, ID, self.__mail_doc) 1566 1567 ID = wx.NewId() 1568 self.__doc_context_menu.Append(ID, _('Export all parts')) 1569 wx.EVT_MENU(self.__doc_context_menu, ID, self.__export_doc_to_disk) 1570 1571 self.__doc_context_menu.AppendSeparator() 1572 1573 ID = wx.NewId() 1574 self.__doc_context_menu.Append(ID, _('Delete document')) 1575 wx.EVT_MENU(self.__doc_context_menu, ID, self.__delete_document) 1576 1577 ID = wx.NewId() 1578 self.__doc_context_menu.Append(ID, _('Access external original')) 1579 wx.EVT_MENU(self.__doc_context_menu, ID, self.__access_external_original) 1580 1581 ID = wx.NewId() 1582 self.__doc_context_menu.Append(ID, _('Edit corresponding encounter')) 1583 wx.EVT_MENU(self.__doc_context_menu, ID, self.__edit_encounter_details) 1584 1585 ID = wx.NewId() 1586 self.__doc_context_menu.Append(ID, _('Select corresponding encounter')) 1587 wx.EVT_MENU(self.__doc_context_menu, ID, self.__select_encounter) 1588 1589 # self.__doc_context_menu.AppendSeparator() 1590 1591 ID = wx.NewId() 1592 self.__doc_context_menu.Append(ID, _('Manage descriptions')) 1593 wx.EVT_MENU(self.__doc_context_menu, ID, self.__manage_document_descriptions)
1594 1595 # document / description 1596 # self.__desc_menu = wx.Menu() 1597 # ID = wx.NewId() 1598 # self.__doc_context_menu.AppendMenu(ID, _('Descriptions ...'), self.__desc_menu) 1599 1600 # ID = wx.NewId() 1601 # self.__desc_menu.Append(ID, _('Add new description')) 1602 # wx.EVT_MENU(self.__desc_menu, ID, self.__add_doc_desc) 1603 1604 # ID = wx.NewId() 1605 # self.__desc_menu.Append(ID, _('Delete description')) 1606 # wx.EVT_MENU(self.__desc_menu, ID, self.__del_doc_desc) 1607 1608 # self.__desc_menu.AppendSeparator() 1609 #--------------------------------------------------------
1610 - def __populate_tree(self):
1611 1612 wx.BeginBusyCursor() 1613 1614 # clean old tree 1615 if self.root is not None: 1616 self.DeleteAllItems() 1617 1618 # init new tree 1619 self.root = self.AddRoot(cDocTree._root_node_labels[self.__sort_mode], -1, -1) 1620 self.SetItemPyData(self.root, None) 1621 self.SetItemHasChildren(self.root, False) 1622 1623 # read documents from database 1624 curr_pat = gmPerson.gmCurrentPatient() 1625 docs_folder = curr_pat.get_document_folder() 1626 docs = docs_folder.get_documents() 1627 1628 if docs is None: 1629 gmGuiHelpers.gm_show_error ( 1630 aMessage = _('Error searching documents.'), 1631 aTitle = _('loading document list') 1632 ) 1633 # avoid recursion of GUI updating 1634 wx.EndBusyCursor() 1635 return True 1636 1637 if len(docs) == 0: 1638 wx.EndBusyCursor() 1639 return True 1640 1641 # fill new tree from document list 1642 self.SetItemHasChildren(self.root, True) 1643 1644 # add our documents as first level nodes 1645 intermediate_nodes = {} 1646 for doc in docs: 1647 1648 parts = doc.parts 1649 1650 label = _('%s%7s %s:%s (%s part(s)%s)') % ( 1651 gmTools.bool2subst(doc.has_unreviewed_parts, gmTools.u_writing_hand, u'', u'?'), 1652 doc['clin_when'].strftime('%m/%Y'), 1653 doc['l10n_type'][:26], 1654 gmTools.coalesce(initial = doc['comment'], instead = u'', template_initial = u' %s'), 1655 len(parts), 1656 gmTools.coalesce(initial = doc['ext_ref'], instead = u'', template_initial = u', \u00BB%s\u00AB') 1657 ) 1658 1659 # need intermediate branch level ? 1660 if self.__sort_mode == 'episode': 1661 lbl = u'%s%s' % (doc['episode'], gmTools.coalesce(doc['health_issue'], u'', u' (%s)')) 1662 if not intermediate_nodes.has_key(lbl): 1663 intermediate_nodes[lbl] = self.AppendItem(parent = self.root, text = lbl) 1664 self.SetItemBold(intermediate_nodes[lbl], bold = True) 1665 self.SetItemPyData(intermediate_nodes[lbl], None) 1666 self.SetItemHasChildren(intermediate_nodes[lbl], True) 1667 parent = intermediate_nodes[lbl] 1668 elif self.__sort_mode == 'type': 1669 lbl = doc['l10n_type'] 1670 if not intermediate_nodes.has_key(lbl): 1671 intermediate_nodes[lbl] = self.AppendItem(parent = self.root, text = lbl) 1672 self.SetItemBold(intermediate_nodes[lbl], bold = True) 1673 self.SetItemPyData(intermediate_nodes[lbl], None) 1674 self.SetItemHasChildren(intermediate_nodes[lbl], True) 1675 parent = intermediate_nodes[lbl] 1676 elif self.__sort_mode == 'issue': 1677 if doc['health_issue'] is None: 1678 lbl = _('Unattributed episode: %s') % doc['episode'] 1679 else: 1680 lbl = doc['health_issue'] 1681 if not intermediate_nodes.has_key(lbl): 1682 intermediate_nodes[lbl] = self.AppendItem(parent = self.root, text = lbl) 1683 self.SetItemBold(intermediate_nodes[lbl], bold = True) 1684 self.SetItemPyData(intermediate_nodes[lbl], None) 1685 self.SetItemHasChildren(intermediate_nodes[lbl], True) 1686 parent = intermediate_nodes[lbl] 1687 else: 1688 parent = self.root 1689 1690 doc_node = self.AppendItem(parent = parent, text = label) 1691 #self.SetItemBold(doc_node, bold = True) 1692 self.SetItemPyData(doc_node, doc) 1693 if len(parts) == 0: 1694 self.SetItemHasChildren(doc_node, False) 1695 else: 1696 self.SetItemHasChildren(doc_node, True) 1697 1698 # now add parts as child nodes 1699 for part in parts: 1700 # if part['clinically_relevant']: 1701 # rel = ' [%s]' % _('Cave') 1702 # else: 1703 # rel = '' 1704 f_ext = u'' 1705 if part['filename'] is not None: 1706 f_ext = os.path.splitext(part['filename'])[1].strip('.').strip() 1707 if f_ext != u'': 1708 f_ext = u' .' + f_ext.upper() 1709 label = '%s%s (%s%s)%s' % ( 1710 gmTools.bool2str ( 1711 boolean = part['reviewed'] or part['reviewed_by_you'] or part['reviewed_by_intended_reviewer'], 1712 true_str = u'', 1713 false_str = gmTools.u_writing_hand 1714 ), 1715 _('part %2s') % part['seq_idx'], 1716 gmTools.size2str(part['size']), 1717 f_ext, 1718 gmTools.coalesce ( 1719 part['obj_comment'], 1720 u'', 1721 u': %s%%s%s' % (gmTools.u_left_double_angle_quote, gmTools.u_right_double_angle_quote) 1722 ) 1723 ) 1724 1725 part_node = self.AppendItem(parent = doc_node, text = label) 1726 self.SetItemPyData(part_node, part) 1727 self.SetItemHasChildren(part_node, False) 1728 1729 self.__sort_nodes() 1730 self.SelectItem(self.root) 1731 1732 # FIXME: apply expansion state if available or else ... 1733 # FIXME: ... uncollapse to default state 1734 self.Expand(self.root) 1735 if self.__sort_mode in ['episode', 'type', 'issue']: 1736 for key in intermediate_nodes.keys(): 1737 self.Expand(intermediate_nodes[key]) 1738 1739 wx.EndBusyCursor() 1740 1741 return True
1742 #------------------------------------------------------------------------
1743 - def OnCompareItems (self, node1=None, node2=None):
1744 """Used in sorting items. 1745 1746 -1: 1 < 2 1747 0: 1 = 2 1748 1: 1 > 2 1749 """ 1750 # Windows can send bogus events so ignore that 1751 if not node1: 1752 _log.debug('invalid node 1') 1753 return 0 1754 if not node2: 1755 _log.debug('invalid node 2') 1756 return 0 1757 if not node1.IsOk(): 1758 _log.debug('no data on node 1') 1759 return 0 1760 if not node2.IsOk(): 1761 _log.debug('no data on node 2') 1762 return 0 1763 1764 data1 = self.GetPyData(node1) 1765 data2 = self.GetPyData(node2) 1766 1767 # doc node 1768 if isinstance(data1, gmDocuments.cDocument): 1769 1770 date_field = 'clin_when' 1771 #date_field = 'modified_when' 1772 1773 if self.__sort_mode == 'age': 1774 # reverse sort by date 1775 if data1[date_field] > data2[date_field]: 1776 return -1 1777 if data1[date_field] == data2[date_field]: 1778 return 0 1779 return 1 1780 1781 elif self.__sort_mode == 'episode': 1782 if data1['episode'] < data2['episode']: 1783 return -1 1784 if data1['episode'] == data2['episode']: 1785 # inner sort: reverse by date 1786 if data1[date_field] > data2[date_field]: 1787 return -1 1788 if data1[date_field] == data2[date_field]: 1789 return 0 1790 return 1 1791 return 1 1792 1793 elif self.__sort_mode == 'issue': 1794 if data1['health_issue'] < data2['health_issue']: 1795 return -1 1796 if data1['health_issue'] == data2['health_issue']: 1797 # inner sort: reverse 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 return 1 1804 1805 elif self.__sort_mode == 'review': 1806 # equality 1807 if data1.has_unreviewed_parts == data2.has_unreviewed_parts: 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 if data1.has_unreviewed_parts: 1815 return -1 1816 return 1 1817 1818 elif self.__sort_mode == 'type': 1819 if data1['l10n_type'] < data2['l10n_type']: 1820 return -1 1821 if data1['l10n_type'] == data2['l10n_type']: 1822 # inner sort: reverse by date 1823 if data1[date_field] > data2[date_field]: 1824 return -1 1825 if data1[date_field] == data2[date_field]: 1826 return 0 1827 return 1 1828 return 1 1829 1830 else: 1831 _log.error('unknown document sort mode [%s], reverse-sorting by age', self.__sort_mode) 1832 # reverse sort by date 1833 if data1[date_field] > data2[date_field]: 1834 return -1 1835 if data1[date_field] == data2[date_field]: 1836 return 0 1837 return 1 1838 1839 # part node 1840 if isinstance(data1, gmDocuments.cDocumentPart): 1841 # compare sequence IDs (= "page" numbers) 1842 # FIXME: wrong order ? 1843 if data1['seq_idx'] < data2['seq_idx']: 1844 return -1 1845 if data1['seq_idx'] == data2['seq_idx']: 1846 return 0 1847 return 1 1848 1849 # else sort alphabetically 1850 if None in [data1, data2]: 1851 l1 = self.GetItemText(node1) 1852 l2 = self.GetItemText(node2) 1853 if l1 < l2: 1854 return -1 1855 if l1 == l2: 1856 return 0 1857 else: 1858 if data1 < data2: 1859 return -1 1860 if data1 == data2: 1861 return 0 1862 return 1
1863 #------------------------------------------------------------------------ 1864 # event handlers 1865 #------------------------------------------------------------------------
1866 - def _on_doc_mod_db(self, *args, **kwargs):
1867 # FIXME: remember current expansion state 1868 wx.CallAfter(self._schedule_data_reget)
1869 #------------------------------------------------------------------------
1870 - def _on_doc_page_mod_db(self, *args, **kwargs):
1871 # FIXME: remember current expansion state 1872 wx.CallAfter(self._schedule_data_reget)
1873 #------------------------------------------------------------------------
1874 - def _on_pre_patient_selection(self, *args, **kwargs):
1875 # FIXME: self.__store_expansion_history_in_db 1876 1877 # empty out tree 1878 if self.root is not None: 1879 self.DeleteAllItems() 1880 self.root = None
1881 #------------------------------------------------------------------------
1882 - def _on_post_patient_selection(self, *args, **kwargs):
1883 # FIXME: self.__load_expansion_history_from_db (but not apply it !) 1884 self._schedule_data_reget()
1885 #------------------------------------------------------------------------
1886 - def _on_activate(self, event):
1887 node = event.GetItem() 1888 node_data = self.GetPyData(node) 1889 1890 # exclude pseudo root node 1891 if node_data is None: 1892 return None 1893 1894 # expand/collapse documents on activation 1895 if isinstance(node_data, gmDocuments.cDocument): 1896 self.Toggle(node) 1897 return True 1898 1899 # string nodes are labels such as episodes which may or may not have children 1900 if type(node_data) == type('string'): 1901 self.Toggle(node) 1902 return True 1903 1904 self.__display_part(part = node_data) 1905 return True
1906 #--------------------------------------------------------
1907 - def __on_right_click(self, evt):
1908 1909 node = evt.GetItem() 1910 self.__curr_node_data = self.GetPyData(node) 1911 1912 # exclude pseudo root node 1913 if self.__curr_node_data is None: 1914 return None 1915 1916 # documents 1917 if isinstance(self.__curr_node_data, gmDocuments.cDocument): 1918 self.__handle_doc_context() 1919 1920 # parts 1921 if isinstance(self.__curr_node_data, gmDocuments.cDocumentPart): 1922 self.__handle_part_context() 1923 1924 del self.__curr_node_data 1925 evt.Skip()
1926 #--------------------------------------------------------
1927 - def __activate_as_current_photo(self, evt):
1928 self.__curr_node_data.set_as_active_photograph()
1929 #--------------------------------------------------------
1930 - def __display_curr_part(self, evt):
1931 self.__display_part(part = self.__curr_node_data)
1932 #--------------------------------------------------------
1933 - def __review_curr_part(self, evt):
1934 self.__review_part(part = self.__curr_node_data)
1935 #--------------------------------------------------------
1936 - def __manage_document_descriptions(self, evt):
1937 manage_document_descriptions(parent = self, document = self.__curr_node_data)
1938 #-------------------------------------------------------- 1939 # internal API 1940 #--------------------------------------------------------
1941 - def __sort_nodes(self, start_node=None):
1942 1943 if start_node is None: 1944 start_node = self.GetRootItem() 1945 1946 # protect against empty tree where not even 1947 # a root node exists 1948 if not start_node.IsOk(): 1949 return True 1950 1951 self.SortChildren(start_node) 1952 1953 child_node, cookie = self.GetFirstChild(start_node) 1954 while child_node.IsOk(): 1955 self.__sort_nodes(start_node = child_node) 1956 child_node, cookie = self.GetNextChild(start_node, cookie) 1957 1958 return
1959 #--------------------------------------------------------
1960 - def __handle_doc_context(self):
1961 self.PopupMenu(self.__doc_context_menu, wx.DefaultPosition)
1962 #--------------------------------------------------------
1963 - def __handle_part_context(self):
1964 # make active patient photograph 1965 if self.__curr_node_data['type'] == 'patient photograph': 1966 ID = wx.NewId() 1967 self.__part_context_menu.Append(ID, _('Activate as current photo')) 1968 wx.EVT_MENU(self.__part_context_menu, ID, self.__activate_as_current_photo) 1969 else: 1970 ID = None 1971 1972 self.PopupMenu(self.__part_context_menu, wx.DefaultPosition) 1973 1974 if ID is not None: 1975 self.__part_context_menu.Delete(ID)
1976 #-------------------------------------------------------- 1977 # part level context menu handlers 1978 #--------------------------------------------------------
1979 - def __display_part(self, part):
1980 """Display document part.""" 1981 1982 # sanity check 1983 if part['size'] == 0: 1984 _log.debug('cannot display part [%s] - 0 bytes', part['pk_obj']) 1985 gmGuiHelpers.gm_show_error ( 1986 aMessage = _('Document part does not seem to exist in database !'), 1987 aTitle = _('showing document') 1988 ) 1989 return None 1990 1991 wx.BeginBusyCursor() 1992 1993 cfg = gmCfg.cCfgSQL() 1994 1995 # determine database export chunk size 1996 chunksize = int( 1997 cfg.get2 ( 1998 option = "horstspace.blob_export_chunk_size", 1999 workplace = gmSurgery.gmCurrentPractice().active_workplace, 2000 bias = 'workplace', 2001 default = default_chunksize 2002 )) 2003 2004 # shall we force blocking during view ? 2005 block_during_view = bool( cfg.get2 ( 2006 option = 'horstspace.document_viewer.block_during_view', 2007 workplace = gmSurgery.gmCurrentPractice().active_workplace, 2008 bias = 'user', 2009 default = None 2010 )) 2011 2012 # display it 2013 successful, msg = part.display_via_mime ( 2014 chunksize = chunksize, 2015 block = block_during_view 2016 ) 2017 2018 wx.EndBusyCursor() 2019 2020 if not successful: 2021 gmGuiHelpers.gm_show_error ( 2022 aMessage = _('Cannot display document part:\n%s') % msg, 2023 aTitle = _('showing document') 2024 ) 2025 return None 2026 2027 # handle review after display 2028 # 0: never 2029 # 1: always 2030 # 2: if no review by myself exists yet 2031 # 3: if no review at all exists yet 2032 # 4: if no review by responsible reviewer 2033 review_after_display = int(cfg.get2 ( 2034 option = 'horstspace.document_viewer.review_after_display', 2035 workplace = gmSurgery.gmCurrentPractice().active_workplace, 2036 bias = 'user', 2037 default = 3 2038 )) 2039 if review_after_display == 1: # always review 2040 self.__review_part(part=part) 2041 elif review_after_display == 2: # review if no review by me exists 2042 review_by_me = filter(lambda rev: rev['is_your_review'], part.get_reviews()) 2043 if len(review_by_me) == 0: 2044 self.__review_part(part = part) 2045 elif review_after_display == 3: 2046 if len(part.get_reviews()) == 0: 2047 self.__review_part(part = part) 2048 elif review_after_display == 4: 2049 reviewed_by_responsible = filter(lambda rev: rev['is_review_by_responsible_reviewer'], part.get_reviews()) 2050 if len(reviewed_by_responsible) == 0: 2051 self.__review_part(part = part) 2052 2053 return True
2054 #--------------------------------------------------------
2055 - def __review_part(self, part=None):
2056 dlg = cReviewDocPartDlg ( 2057 parent = self, 2058 id = -1, 2059 part = part 2060 ) 2061 dlg.ShowModal() 2062 dlg.Destroy()
2063 #--------------------------------------------------------
2064 - def __move_part(self, evt):
2065 target_doc = manage_documents ( 2066 parent = self, 2067 msg = _('\nSelect the document into which to move the selected part !\n') 2068 ) 2069 if target_doc is None: 2070 return 2071 self.__curr_node_data['pk_doc'] = target_doc['pk_doc'] 2072 self.__curr_node_data.save()
2073 #--------------------------------------------------------
2074 - def __delete_part(self, evt):
2075 delete_it = gmGuiHelpers.gm_show_question ( 2076 cancel_button = True, 2077 title = _('Deleting document part'), 2078 question = _( 2079 'Are you sure you want to delete the %s part #%s\n' 2080 '\n' 2081 '%s' 2082 'from the following document\n' 2083 '\n' 2084 ' %s (%s)\n' 2085 '%s' 2086 '\n' 2087 'Really delete ?\n' 2088 '\n' 2089 '(this action cannot be reversed)' 2090 ) % ( 2091 gmTools.size2str(self.__curr_node_data['size']), 2092 self.__curr_node_data['seq_idx'], 2093 gmTools.coalesce(self.__curr_node_data['obj_comment'], u'', u' "%s"\n\n'), 2094 self.__curr_node_data['l10n_type'], 2095 gmDateTime.pydt_strftime(self.__curr_node_data['date_generated'], format = '%Y-%m-%d', accuracy = gmDateTime.acc_days), 2096 gmTools.coalesce(self.__curr_node_data['doc_comment'], u'', u' "%s"\n') 2097 ) 2098 ) 2099 if not delete_it: 2100 return 2101 2102 gmDocuments.delete_document_part ( 2103 part_pk = self.__curr_node_data['pk_obj'], 2104 encounter_pk = gmPerson.gmCurrentPatient().emr.active_encounter['pk_encounter'] 2105 )
2106 #--------------------------------------------------------
2107 - def __process_part(self, action=None, l10n_action=None):
2108 2109 gmHooks.run_hook_script(hook = u'before_%s_doc_part' % action) 2110 2111 wx.BeginBusyCursor() 2112 2113 # detect wrapper 2114 found, external_cmd = gmShellAPI.detect_external_binary(u'gm-%s_doc' % action) 2115 if not found: 2116 found, external_cmd = gmShellAPI.detect_external_binary(u'gm-%s_doc.bat' % action) 2117 if not found: 2118 _log.error('neither of gm-%s_doc or gm-%s_doc.bat found', action, action) 2119 wx.EndBusyCursor() 2120 gmGuiHelpers.gm_show_error ( 2121 _('Cannot %(l10n_action)s document part - %(l10n_action)s command not found.\n' 2122 '\n' 2123 'Either of gm_%(action)s_doc.sh or gm_%(action)s_doc.bat\n' 2124 'must be in the execution path. The command will\n' 2125 'be passed the filename to %(l10n_action)s.' 2126 ) % {'action': action, 'l10n_action': l10n_action}, 2127 _('Processing document part: %s') % l10n_action 2128 ) 2129 return 2130 2131 cfg = gmCfg.cCfgSQL() 2132 2133 # determine database export chunk size 2134 chunksize = int(cfg.get2 ( 2135 option = "horstspace.blob_export_chunk_size", 2136 workplace = gmSurgery.gmCurrentPractice().active_workplace, 2137 bias = 'workplace', 2138 default = default_chunksize 2139 )) 2140 2141 part_file = self.__curr_node_data.export_to_file ( 2142 # aTempDir = tmp_dir, 2143 aChunkSize = chunksize 2144 ) 2145 2146 cmd = u'%s %s' % (external_cmd, part_file) 2147 if os.name == 'nt': 2148 blocking = True 2149 else: 2150 blocking = False 2151 success = gmShellAPI.run_command_in_shell ( 2152 command = cmd, 2153 blocking = blocking 2154 ) 2155 2156 wx.EndBusyCursor() 2157 2158 if not success: 2159 _log.error('%s command failed: [%s]', action, cmd) 2160 gmGuiHelpers.gm_show_error ( 2161 _('Cannot %(l10n_action)s document part - %(l10n_action)s command failed.\n' 2162 '\n' 2163 'You may need to check and fix either of\n' 2164 ' gm_%(action)s_doc.sh (Unix/Mac) or\n' 2165 ' gm_%(action)s_doc.bat (Windows)\n' 2166 '\n' 2167 'The command is passed the filename to %(l10n_action)s.' 2168 ) % {'action': action, 'l10n_action': l10n_action}, 2169 _('Processing document part: %s') % l10n_action 2170 )
2171 #--------------------------------------------------------
2172 - def __print_part(self, evt):
2173 self.__process_part(action = u'print', l10n_action = _('print'))
2174 #--------------------------------------------------------
2175 - def __fax_part(self, evt):
2176 self.__process_part(action = u'fax', l10n_action = _('fax'))
2177 #--------------------------------------------------------
2178 - def __mail_part(self, evt):
2179 self.__process_part(action = u'mail', l10n_action = _('mail'))
2180 #-------------------------------------------------------- 2181 # document level context menu handlers 2182 #--------------------------------------------------------
2183 - def __select_encounter(self, evt):
2184 enc = gmEMRStructWidgets.select_encounters ( 2185 parent = self, 2186 patient = gmPerson.gmCurrentPatient() 2187 ) 2188 if not enc: 2189 return 2190 self.__curr_node_data['pk_encounter'] = enc['pk_encounter'] 2191 self.__curr_node_data.save()
2192 #--------------------------------------------------------
2193 - def __edit_encounter_details(self, evt):
2194 enc = gmEMRStructItems.cEncounter(aPK_obj = self.__curr_node_data['pk_encounter']) 2195 gmEMRStructWidgets.edit_encounter(parent = self, encounter = enc)
2196 #--------------------------------------------------------
2197 - def __process_doc(self, action=None, l10n_action=None):
2198 2199 gmHooks.run_hook_script(hook = u'before_%s_doc' % action) 2200 2201 wx.BeginBusyCursor() 2202 2203 # detect wrapper 2204 found, external_cmd = gmShellAPI.detect_external_binary(u'gm-%s_doc' % action) 2205 if not found: 2206 found, external_cmd = gmShellAPI.detect_external_binary(u'gm-%s_doc.bat' % action) 2207 if not found: 2208 _log.error('neither of gm-%s_doc or gm-%s_doc.bat found', action, action) 2209 wx.EndBusyCursor() 2210 gmGuiHelpers.gm_show_error ( 2211 _('Cannot %(l10n_action)s document - %(l10n_action)s command not found.\n' 2212 '\n' 2213 'Either of gm_%(action)s_doc.sh or gm_%(action)s_doc.bat\n' 2214 'must be in the execution path. The command will\n' 2215 'be passed a list of filenames to %(l10n_action)s.' 2216 ) % {'action': action, 'l10n_action': l10n_action}, 2217 _('Processing document: %s') % l10n_action 2218 ) 2219 return 2220 2221 cfg = gmCfg.cCfgSQL() 2222 2223 # determine database export chunk size 2224 chunksize = int(cfg.get2 ( 2225 option = "horstspace.blob_export_chunk_size", 2226 workplace = gmSurgery.gmCurrentPractice().active_workplace, 2227 bias = 'workplace', 2228 default = default_chunksize 2229 )) 2230 2231 part_files = self.__curr_node_data.export_parts_to_files(chunksize = chunksize) 2232 2233 if os.name == 'nt': 2234 blocking = True 2235 else: 2236 blocking = False 2237 cmd = external_cmd + u' ' + u' '.join(part_files) 2238 success = gmShellAPI.run_command_in_shell ( 2239 command = cmd, 2240 blocking = blocking 2241 ) 2242 2243 wx.EndBusyCursor() 2244 2245 if not success: 2246 _log.error('%s command failed: [%s]', action, cmd) 2247 gmGuiHelpers.gm_show_error ( 2248 _('Cannot %(l10n_action)s document - %(l10n_action)s command failed.\n' 2249 '\n' 2250 'You may need to check and fix either of\n' 2251 ' gm_%(action)s_doc.sh (Unix/Mac) or\n' 2252 ' gm_%(action)s_doc.bat (Windows)\n' 2253 '\n' 2254 'The command is passed a list of filenames to %(l10n_action)s.' 2255 ) % {'action': action, 'l10n_action': l10n_action}, 2256 _('Processing document: %s') % l10n_action 2257 )
2258 #-------------------------------------------------------- 2259 # FIXME: icons in the plugin toolbar
2260 - def __print_doc(self, evt):
2261 self.__process_doc(action = u'print', l10n_action = _('print'))
2262 #--------------------------------------------------------
2263 - def __fax_doc(self, evt):
2264 self.__process_doc(action = u'fax', l10n_action = _('fax'))
2265 #--------------------------------------------------------
2266 - def __mail_doc(self, evt):
2267 self.__process_doc(action = u'mail', l10n_action = _('mail'))
2268 #--------------------------------------------------------
2269 - def __add_part(self, evt):
2270 dlg = wx.FileDialog ( 2271 parent = self, 2272 message = _('Choose a file'), 2273 defaultDir = os.path.expanduser(os.path.join('~', 'gnumed')), 2274 defaultFile = '', 2275 wildcard = "%s (*)|*|PNGs (*.png)|*.png|PDFs (*.pdf)|*.pdf|TIFFs (*.tif)|*.tif|JPEGs (*.jpg)|*.jpg|%s (*.*)|*.*" % (_('all files'), _('all files (Win)')), 2276 style = wx.OPEN | wx.FILE_MUST_EXIST | wx.MULTIPLE 2277 ) 2278 result = dlg.ShowModal() 2279 if result != wx.ID_CANCEL: 2280 self.__curr_node_data.add_parts_from_files(files = dlg.GetPaths(), reviewer = gmStaff.gmCurrentProvider()['pk_staff']) 2281 dlg.Destroy()
2282 #--------------------------------------------------------
2283 - def __access_external_original(self, evt):
2284 2285 gmHooks.run_hook_script(hook = u'before_external_doc_access') 2286 2287 wx.BeginBusyCursor() 2288 2289 # detect wrapper 2290 found, external_cmd = gmShellAPI.detect_external_binary(u'gm_access_external_doc.sh') 2291 if not found: 2292 found, external_cmd = gmShellAPI.detect_external_binary(u'gm_access_external_doc.bat') 2293 if not found: 2294 _log.error('neither of gm_access_external_doc.sh or .bat found') 2295 wx.EndBusyCursor() 2296 gmGuiHelpers.gm_show_error ( 2297 _('Cannot access external document - access command not found.\n' 2298 '\n' 2299 'Either of gm_access_external_doc.sh or *.bat must be\n' 2300 'in the execution path. The command will be passed the\n' 2301 'document type and the reference URL for processing.' 2302 ), 2303 _('Accessing external document') 2304 ) 2305 return 2306 2307 cmd = u'%s "%s" "%s"' % (external_cmd, self.__curr_node_data['type'], self.__curr_node_data['ext_ref']) 2308 if os.name == 'nt': 2309 blocking = True 2310 else: 2311 blocking = False 2312 success = gmShellAPI.run_command_in_shell ( 2313 command = cmd, 2314 blocking = blocking 2315 ) 2316 2317 wx.EndBusyCursor() 2318 2319 if not success: 2320 _log.error('External access command failed: [%s]', cmd) 2321 gmGuiHelpers.gm_show_error ( 2322 _('Cannot access external document - access command failed.\n' 2323 '\n' 2324 'You may need to check and fix either of\n' 2325 ' gm_access_external_doc.sh (Unix/Mac) or\n' 2326 ' gm_access_external_doc.bat (Windows)\n' 2327 '\n' 2328 'The command is passed the document type and the\n' 2329 'external reference URL on the command line.' 2330 ), 2331 _('Accessing external document') 2332 )
2333 #--------------------------------------------------------
2334 - def __export_doc_to_disk(self, evt):
2335 """Export document into directory. 2336 2337 - one file per object 2338 - into subdirectory named after patient 2339 """ 2340 pat = gmPerson.gmCurrentPatient() 2341 dname = '%s-%s%s' % ( 2342 self.__curr_node_data['l10n_type'], 2343 self.__curr_node_data['clin_when'].strftime('%Y-%m-%d'), 2344 gmTools.coalesce(self.__curr_node_data['ext_ref'], '', '-%s').replace(' ', '_') 2345 ) 2346 def_dir = os.path.expanduser(os.path.join('~', 'gnumed', 'export', 'docs', pat['dirname'], dname)) 2347 gmTools.mkdir(def_dir) 2348 2349 dlg = wx.DirDialog ( 2350 parent = self, 2351 message = _('Save document into directory ...'), 2352 defaultPath = def_dir, 2353 style = wx.DD_DEFAULT_STYLE 2354 ) 2355 result = dlg.ShowModal() 2356 dirname = dlg.GetPath() 2357 dlg.Destroy() 2358 2359 if result != wx.ID_OK: 2360 return True 2361 2362 wx.BeginBusyCursor() 2363 2364 cfg = gmCfg.cCfgSQL() 2365 2366 # determine database export chunk size 2367 chunksize = int(cfg.get2 ( 2368 option = "horstspace.blob_export_chunk_size", 2369 workplace = gmSurgery.gmCurrentPractice().active_workplace, 2370 bias = 'workplace', 2371 default = default_chunksize 2372 )) 2373 2374 fnames = self.__curr_node_data.export_parts_to_files(export_dir = dirname, chunksize = chunksize) 2375 2376 wx.EndBusyCursor() 2377 2378 gmDispatcher.send(signal='statustext', msg=_('Successfully exported %s parts into the directory [%s].') % (len(fnames), dirname)) 2379 2380 return True
2381 #--------------------------------------------------------
2382 - def __delete_document(self, evt):
2383 result = gmGuiHelpers.gm_show_question ( 2384 aMessage = _('Are you sure you want to delete the document ?'), 2385 aTitle = _('Deleting document') 2386 ) 2387 if result is True: 2388 curr_pat = gmPerson.gmCurrentPatient() 2389 emr = curr_pat.get_emr() 2390 enc = emr.active_encounter 2391 gmDocuments.delete_document(document_id = self.__curr_node_data['pk_doc'], encounter_id = enc['pk_encounter'])
2392 #============================================================ 2393 # main 2394 #------------------------------------------------------------ 2395 if __name__ == '__main__': 2396 2397 gmI18N.activate_locale() 2398 gmI18N.install_domain(domain = 'gnumed') 2399 2400 #---------------------------------------- 2401 #---------------------------------------- 2402 if (len(sys.argv) > 1) and (sys.argv[1] == 'test'): 2403 # test_*() 2404 pass 2405 2406 #============================================================ 2407