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