Package Gnumed :: Package wxpython :: Module gmVisualProgressNoteWidgets
[frames] | no frames]

Source Code for Module Gnumed.wxpython.gmVisualProgressNoteWidgets

  1  # -*- coding: utf-8 -*- 
  2  """GNUmed visual progress notes handling widgets.""" 
  3  #================================================================ 
  4  __author__ = "Karsten Hilbert <Karsten.Hilbert@gmx.net>" 
  5  __license__ = "GPL v2 or later (details at http://www.gnu.org)" 
  6   
  7  import sys 
  8  import logging 
  9  import os 
 10  import os.path 
 11  import shutil 
 12  import random 
 13  import threading 
 14   
 15   
 16  import wx 
 17  import wx.lib.agw.supertooltip as agw_stt 
 18  import wx.lib.statbmp as wx_genstatbmp 
 19   
 20   
 21  if __name__ == '__main__': 
 22          sys.path.insert(0, '../../') 
 23   
 24  from Gnumed.pycommon import gmI18N 
 25   
 26  if __name__ == '__main__': 
 27          gmI18N.activate_locale() 
 28          gmI18N.install_domain() 
 29   
 30  from Gnumed.pycommon import gmDispatcher 
 31  from Gnumed.pycommon import gmTools 
 32  from Gnumed.pycommon import gmDateTime 
 33  from Gnumed.pycommon import gmShellAPI 
 34  from Gnumed.pycommon import gmCfg 
 35  from Gnumed.pycommon import gmMatchProvider 
 36  from Gnumed.pycommon import gmMimeLib 
 37  from Gnumed.pycommon import gmWorkerThread 
 38  from Gnumed.pycommon import gmPG2 
 39   
 40  from Gnumed.business import gmPerson 
 41  from Gnumed.business import gmEMRStructItems 
 42  from Gnumed.business import gmPraxis 
 43  from Gnumed.business import gmForms 
 44  from Gnumed.business import gmDocuments 
 45   
 46  from Gnumed.wxpython import gmPhraseWheel 
 47  from Gnumed.wxpython import gmGuiHelpers 
 48  from Gnumed.wxpython import gmCfgWidgets 
 49  from Gnumed.wxpython import gmDocumentWidgets 
 50   
 51   
 52  _log = logging.getLogger('gm.ui') 
 53   
 54  #============================================================ 
 55  # visual progress notes 
 56  #============================================================ 
57 -def configure_visual_progress_note_editor():
58 59 def is_valid(value): 60 61 if value is None: 62 gmDispatcher.send ( 63 signal = 'statustext', 64 msg = _('You need to actually set an editor.'), 65 beep = True 66 ) 67 return False, value 68 69 if value.strip() == '': 70 gmDispatcher.send ( 71 signal = 'statustext', 72 msg = _('You need to actually set an editor.'), 73 beep = True 74 ) 75 return False, value 76 77 found, binary = gmShellAPI.detect_external_binary(value) 78 if not found: 79 gmDispatcher.send ( 80 signal = 'statustext', 81 msg = _('The command [%s] is not found.') % value, 82 beep = True 83 ) 84 return True, value 85 86 return True, binary
87 88 #------------------------------------------ 89 cmd = gmCfgWidgets.configure_string_option ( 90 message = _( 91 'Enter the shell command with which to start\n' 92 'the image editor for visual progress notes.\n' 93 '\n' 94 'Any "%(img)s" included with the arguments\n' 95 'will be replaced by the file name of the\n' 96 'note template.' 97 ), 98 option = 'external.tools.visual_soap_editor_cmd', 99 bias = 'user', 100 default_value = None, 101 validator = is_valid 102 ) 103 104 return cmd 105 106 #============================================================
107 -def select_file_as_visual_progress_note_template(parent=None):
108 if parent is None: 109 parent = wx.GetApp().GetTopWindow() 110 111 dlg = wx.FileDialog ( 112 parent = parent, 113 message = _('Choose file to use as template for new visual progress note'), 114 defaultDir = os.path.expanduser('~'), 115 defaultFile = '', 116 #wildcard = "%s (*)|*|%s (*.*)|*.*" % (_('all files'), _('all files (Win)')), 117 style = wx.FD_OPEN | wx.FD_FILE_MUST_EXIST 118 ) 119 result = dlg.ShowModal() 120 121 if result == wx.ID_CANCEL: 122 dlg.DestroyLater() 123 return None 124 125 full_filename = dlg.GetPath() 126 dlg.Hide() 127 dlg.DestroyLater() 128 return full_filename
129 130 #------------------------------------------------------------
131 -def select_visual_progress_note_template(parent=None):
132 133 if parent is None: 134 parent = wx.GetApp().GetTopWindow() 135 136 dlg = gmGuiHelpers.c3ButtonQuestionDlg ( 137 parent, 138 -1, 139 caption = _('Visual progress note source'), 140 question = _('From which source do you want to pick the image template ?'), 141 button_defs = [ 142 {'label': _('Database'), 'tooltip': _('List of templates in the database.'), 'default': True}, 143 {'label': _('File'), 'tooltip': _('Files in the filesystem.'), 'default': False}, 144 {'label': _('Device'), 'tooltip': _('Image capture devices (scanners, cameras, etc)'), 'default': False} 145 ] 146 ) 147 result = dlg.ShowModal() 148 dlg.DestroyLater() 149 150 # 1) select from template 151 if result == wx.ID_YES: 152 _log.debug('visual progress note template from: database template') 153 from Gnumed.wxpython import gmFormWidgets 154 template = gmFormWidgets.manage_form_templates ( 155 parent = parent, 156 template_types = [gmDocuments.DOCUMENT_TYPE_VISUAL_PROGRESS_NOTE], 157 active_only = True 158 ) 159 if template is None: 160 return (None, None) 161 filename = template.save_to_file() 162 if filename is None: 163 gmDispatcher.send(signal = 'statustext', msg = _('Cannot export visual progress note template for [%s].') % template['name_long']) 164 return (None, None) 165 return (filename, True) 166 167 # 2) select from disk file 168 if result == wx.ID_NO: 169 _log.debug('visual progress note template from: disk file') 170 fname = select_file_as_visual_progress_note_template(parent = parent) 171 if fname is None: 172 return (None, None) 173 # create a copy of the picked file -- don't modify the original 174 ext = os.path.splitext(fname)[1] 175 tmp_name = gmTools.get_unique_filename(suffix = ext) 176 _log.debug('visual progress note from file: [%s] -> [%s]', fname, tmp_name) 177 shutil.copy2(fname, tmp_name) 178 return (tmp_name, False) 179 180 # 3) acquire from capture device 181 if result == wx.ID_CANCEL: 182 _log.debug('visual progress note template from: image capture device') 183 fnames = gmDocumentWidgets.acquire_images_from_capture_device(device = None, calling_window = parent) 184 if fnames is None: 185 return (None, None) 186 if len(fnames) == 0: 187 return (None, None) 188 return (fnames[0], False) 189 190 _log.debug('no visual progress note template source selected') 191 return (None, None)
192 193 #------------------------------------------------------------
194 -def edit_visual_progress_note(filename=None, episode=None, discard_unmodified=False, doc_part=None, health_issue=None):
195 """This assumes <filename> contains an image which can be handled by the configured image editor.""" 196 197 if doc_part is not None: 198 filename = doc_part.save_to_file() 199 if filename is None: 200 gmDispatcher.send(signal = 'statustext', msg = _('Cannot export visual progress note to file.')) 201 return None 202 203 dbcfg = gmCfg.cCfgSQL() 204 editor = dbcfg.get2 ( 205 option = 'external.tools.visual_soap_editor_cmd', 206 workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace, 207 bias = 'user' 208 ) 209 210 if editor is None: 211 _log.error('no editor for visual progress notes configured, trying mimetype editor') 212 gmDispatcher.send(signal = 'statustext', msg = _('Editor for visual progress note not configured.'), beep = False) 213 mimetype = gmMimeLib.guess_mimetype(filename = filename) 214 editor = gmMimeLib.get_editor_cmd(mimetype = mimetype, filename = filename) 215 if editor is None: 216 _log.error('no editor for mimetype <%s> configured, trying mimetype viewer', mimetype) 217 success, msg = gmMimeLib.call_viewer_on_file(aFile = filename, block = True) 218 if not success: 219 _log.debug('problem running mimetype <%s> viewer', mimetype) 220 gmGuiHelpers.gm_show_error ( 221 _( 'There is no editor for visual progress notes defined.\n' 222 'Also, there is no editor command defined for the file type\n' 223 '\n' 224 ' [%s].\n' 225 '\n' 226 'Therefor GNUmed attempted to at least *show* this\n' 227 'visual progress note. That failed as well, however:\n' 228 '\n' 229 '%s' 230 ) % (mimetype, msg), 231 _('Editing visual progress note') 232 ) 233 editor = configure_visual_progress_note_editor() 234 if editor is None: 235 gmDispatcher.send(signal = 'statustext', msg = _('Editor for visual progress note not configured.'), beep = True) 236 return None 237 238 if '%(img)s' in editor: 239 editor = editor % {'img': filename} 240 else: 241 editor = '%s %s' % (editor, filename) 242 243 if discard_unmodified: 244 original_stat = os.stat(filename) 245 original_md5 = gmTools.file2md5(filename) 246 247 success = gmShellAPI.run_command_in_shell(editor, blocking = True) 248 if not success: 249 success, msg = gmMimeLib.call_viewer_on_file(aFile = filename, block = True) 250 if not success: 251 _log.debug('problem running mimetype <%s> viewer', mimetype) 252 gmGuiHelpers.gm_show_error ( 253 _( 'There was a problem running the editor\n' 254 '\n' 255 ' [%s] (%s)\n' 256 '\n' 257 'on the visual progress note.\n' 258 '\n' 259 'Therefor GNUmed attempted to at least *show* it.\n' 260 'That failed as well, however:\n' 261 '\n' 262 '%s' 263 ) % (editor, mimetype, msg), 264 _('Editing visual progress note') 265 ) 266 editor = configure_visual_progress_note_editor() 267 if editor is None: 268 gmDispatcher.send(signal = 'statustext', msg = _('Editor for visual progress note not configured.'), beep = True) 269 return None 270 271 try: 272 open(filename, 'r').close() 273 except Exception: 274 _log.exception('problem accessing visual progress note file [%s]', filename) 275 gmGuiHelpers.gm_show_error ( 276 _( 'There was a problem reading the visual\n' 277 'progress note from the file:\n' 278 '\n' 279 ' [%s]\n' 280 '\n' 281 ) % filename, 282 _('Saving visual progress note') 283 ) 284 return None 285 286 if discard_unmodified: 287 modified_stat = os.stat(filename) 288 # same size ? 289 if original_stat.st_size == modified_stat.st_size: 290 modified_md5 = gmTools.file2md5(filename) 291 # same hash ? 292 if original_md5 == modified_md5: 293 _log.debug('visual progress note (template) not modified') 294 # ask user to decide 295 msg = _( 296 'You either created a visual progress note from a template\n' 297 'in the database (rather than from a file on disk) or you\n' 298 'edited an existing visual progress note.\n' 299 '\n' 300 'The template/original was not modified at all, however.\n' 301 '\n' 302 'Do you still want to save the unmodified image as a\n' 303 'visual progress note into the EMR of the patient ?\n' 304 ) 305 save_unmodified = gmGuiHelpers.gm_show_question ( 306 msg, 307 _('Saving visual progress note') 308 ) 309 if not save_unmodified: 310 _log.debug('user discarded unmodified note') 311 return 312 313 if doc_part is not None: 314 _log.debug('updating visual progress note') 315 doc_part.update_data_from_file(fname = filename) 316 doc_part.set_reviewed(technically_abnormal = False, clinically_relevant = True) 317 return None 318 319 if not isinstance(episode, gmEMRStructItems.cEpisode): 320 if episode is None: 321 episode = _('visual progress notes') 322 pat = gmPerson.gmCurrentPatient() 323 emr = pat.emr 324 episode = emr.add_episode(episode_name = episode.strip(), pk_health_issue = health_issue, is_open = False) 325 326 doc = gmDocumentWidgets.save_file_as_new_document ( 327 filename = filename, 328 document_type = gmDocuments.DOCUMENT_TYPE_VISUAL_PROGRESS_NOTE, 329 episode = episode, 330 unlock_patient = False, 331 pk_org_unit = gmPraxis.gmCurrentPraxisBranch()['pk_org_unit'], 332 date_generated = gmDateTime.pydt_now_here() 333 ) 334 doc.set_reviewed(technically_abnormal = False, clinically_relevant = True) 335 336 return doc
337 338 #============================================================
339 -class cVisualSoapTemplatePhraseWheel(gmPhraseWheel.cPhraseWheel):
340 """Phrasewheel to allow selection of visual SOAP template.""" 341
342 - def __init__(self, *args, **kwargs):
343 344 gmPhraseWheel.cPhraseWheel.__init__ (self, *args, **kwargs) 345 346 query = """ 347 SELECT 348 pk AS data, 349 name_short AS list_label, 350 name_sort AS field_label 351 FROM 352 ref.paperwork_templates 353 WHERE 354 fk_template_type = (SELECT pk FROM ref.form_types WHERE name = '%s') AND ( 355 name_long %%(fragment_condition)s 356 OR 357 name_short %%(fragment_condition)s 358 ) 359 ORDER BY list_label 360 LIMIT 15 361 """ % gmDocuments.DOCUMENT_TYPE_VISUAL_PROGRESS_NOTE 362 363 mp = gmMatchProvider.cMatchProvider_SQL2(queries = [query]) 364 mp.setThresholds(2, 3, 5) 365 366 self.matcher = mp 367 self.selection_only = True
368 #--------------------------------------------------------
369 - def _data2instance(self):
370 if self.GetData() is None: 371 return None 372 373 return gmForms.cFormTemplate(aPK_obj = self.GetData())
374 375 #============================================================ 376 from Gnumed.wxGladeWidgets import wxgVisualSoapPresenterPnl 377
378 -class cVisualSoapPresenterPnl(wxgVisualSoapPresenterPnl.wxgVisualSoapPresenterPnl):
379 """A panel displaying a number of images (visual progress note thumbnails).""" 380
381 - def __init__(self, *args, **kwargs):
382 wxgVisualSoapPresenterPnl.wxgVisualSoapPresenterPnl.__init__(self, *args, **kwargs) 383 self._SZR_soap = self.GetSizer() 384 self.__bitmaps = []
385 386 #-------------------------------------------------------- 387 # external API 388 #--------------------------------------------------------
389 - def refresh(self, document_folder=None, episodes=None, encounter=None, do_async=False):
390 if not self: 391 # our C/C++ part may be dead already, due to async behaviour 392 return 393 394 if document_folder is None: 395 self.clear() 396 self.GetParent().Layout() 397 return 398 399 soap_docs = document_folder.get_visual_progress_notes(episodes = episodes, encounter = encounter) 400 if len(soap_docs) == 0: 401 self.clear() 402 self.GetParent().Layout() 403 return 404 405 if not do_async: 406 cookie, parts_list = self._worker__export_doc_parts(docs = soap_docs) 407 self.__show_exported_parts(parts_list = parts_list) 408 return 409 410 self.__worker_cookie = '%sCookie-%s' % (self.__class__.__name__, random.random()) 411 _log.debug('starting worker thread, cookie: %s', self.__worker_cookie) 412 gmWorkerThread.execute_in_worker_thread ( 413 payload_function = self._worker__export_doc_parts, 414 payload_kwargs = {'docs': soap_docs, 'cookie': self.__worker_cookie}, 415 completion_callback = self._forwarder__show_exported_doc_parts, 416 worker_name = self.__class__.__name__ 417 )
418 419 #--------------------------------------------------------
420 - def clear(self):
421 if self._SZR_soap: 422 while len(self._SZR_soap.GetChildren()) > 0: 423 self._SZR_soap.Detach(0) 424 for bmp in self.__bitmaps: 425 bmp.Unbind(wx.EVT_LEFT_UP) 426 bmp.DestroyLater() 427 self.__bitmaps = []
428 429 #-------------------------------------------------------- 430 # event handlers 431 #--------------------------------------------------------
432 - def _on_bitmap_leftclicked(self, evt):
433 wx.CallAfter ( 434 edit_visual_progress_note, 435 doc_part = evt.GetEventObject().doc_part, 436 discard_unmodified = True 437 )
438 439 #-------------------------------------------------------- 440 # internal API 441 #--------------------------------------------------------
442 - def _worker__export_doc_parts(self, docs=None, cookie=None):
443 # this is used as the worker thread payload 444 _log.debug('cookie [%s]', cookie) 445 conn = gmPG2.get_connection(readonly = True, connection_name = threading.current_thread().name, pooled = False) 446 parts_list = [] 447 for soap_doc in docs: 448 parts = soap_doc.parts 449 if len(parts) == 0: 450 continue 451 parts_counter = '' 452 if len(parts) > 1: 453 parts_counter = _(' [part 1 of %s]') % len(parts) 454 part = parts[0] 455 fname = part.save_to_file(conn = conn) 456 if fname is None: 457 continue 458 tt_header = _('Created: %s%s') % (gmDateTime.pydt_strftime(part['date_generated'], '%Y %b %d'), parts_counter) 459 tt_footer = gmTools.coalesce(part['doc_comment'], '').strip() 460 parts_list.append([fname, part, tt_header, tt_footer]) 461 conn.close() 462 _log.debug('worker finished') 463 return (cookie, parts_list)
464 465 #--------------------------------------------------------
466 - def _forwarder__show_exported_doc_parts(self, worker_result):
467 # this is the worker thread completion callback 468 cookie, parts_list = worker_result 469 # worker still the one we are interested in ? 470 if cookie != self.__worker_cookie: 471 _log.debug('received results from old worker [%s], I am [%s], ignoring', cookie, self.__worker_cookie) 472 return 473 if len(parts_list) == 0: 474 return 475 wx.CallAfter(self.__show_exported_parts, parts_list = parts_list)
476 477 #--------------------------------------------------------
478 - def __show_exported_parts(self, parts_list=None):
479 self.clear() 480 for part_def in parts_list: 481 fname, part, tt_header, tt_footer = part_def 482 #_log.debug(tt_header) 483 #_log.debug(tt_footer) 484 img = gmGuiHelpers.file2scaled_image ( 485 filename = fname, 486 height = 30 487 ) 488 bmp = wx_genstatbmp.GenStaticBitmap(self, -1, img, style = wx.NO_BORDER) 489 bmp.doc_part = part 490 img = gmGuiHelpers.file2scaled_image ( 491 filename = fname, 492 height = 150 493 ) 494 tip = agw_stt.SuperToolTip ( 495 '', 496 bodyImage = img, 497 header = tt_header, 498 footer = tt_footer 499 ) 500 tip.SetTopGradientColor('white') 501 tip.SetMiddleGradientColor('white') 502 tip.SetBottomGradientColor('white') 503 tip.SetTarget(bmp) 504 bmp.Bind(wx.EVT_LEFT_UP, self._on_bitmap_leftclicked) 505 # FIXME: add context menu for Delete/Clone/Add/Configure 506 self._SZR_soap.Add(bmp, 0, wx.LEFT | wx.RIGHT | wx.TOP | wx.BOTTOM | wx.EXPAND, 3) 507 self.__bitmaps.append(bmp) 508 self.GetParent().Layout()
509 510 #============================================================ 511 # main 512 #------------------------------------------------------------ 513 if __name__ == '__main__': 514 515 if len(sys.argv) < 2: 516 sys.exit() 517 518 if sys.argv[1] != 'test': 519 sys.exit() 520 521 gmI18N.activate_locale() 522 gmI18N.install_domain(domain = 'gnumed') 523 524 #---------------------------------------- 525