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
39
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
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
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):
135
136 -def save_files_as_new_document(parent=None, filenames=None, document_type=None, unlock_patient=False, episode=None, review_as_normal=False, reference=None):
186
187 gmDispatcher.connect(signal = u'import_document_from_file', receiver = _save_file_as_new_document)
188 gmDispatcher.connect(signal = u'import_document_from_files', receiver = _save_files_as_new_document)
189
246
247
248
250
251 if parent is None:
252 parent = wx.GetApp().GetTopWindow()
253
254 dlg = cEditDocumentTypesDlg(parent = parent)
255 dlg.ShowModal()
256
257 from Gnumed.wxGladeWidgets import wxgEditDocumentTypesDlg
258
260 """A dialog showing a cEditDocumentTypesPnl."""
261
264
265
266 from Gnumed.wxGladeWidgets import wxgEditDocumentTypesPnl
267
269 """A panel grouping together fields to edit the list of document types."""
270
276
280
283
286
288
289 self._LCTRL_doc_type.DeleteAllItems()
290
291 doc_types = gmDocuments.get_document_types()
292 pos = len(doc_types) + 1
293
294 for doc_type in doc_types:
295 row_num = self._LCTRL_doc_type.InsertStringItem(pos, label = doc_type['type'])
296 self._LCTRL_doc_type.SetStringItem(index = row_num, col = 1, label = doc_type['l10n_type'])
297 if doc_type['is_user_defined']:
298 self._LCTRL_doc_type.SetStringItem(index = row_num, col = 2, label = ' X ')
299 if doc_type['is_in_use']:
300 self._LCTRL_doc_type.SetStringItem(index = row_num, col = 3, label = ' X ')
301
302 if len(doc_types) > 0:
303 self._LCTRL_doc_type.set_data(data = doc_types)
304 self._LCTRL_doc_type.SetColumnWidth(col=0, width=wx.LIST_AUTOSIZE)
305 self._LCTRL_doc_type.SetColumnWidth(col=1, width=wx.LIST_AUTOSIZE)
306 self._LCTRL_doc_type.SetColumnWidth(col=2, width=wx.LIST_AUTOSIZE_USEHEADER)
307 self._LCTRL_doc_type.SetColumnWidth(col=3, width=wx.LIST_AUTOSIZE_USEHEADER)
308
309 self._TCTRL_type.SetValue('')
310 self._TCTRL_l10n_type.SetValue('')
311
312 self._BTN_set_translation.Enable(False)
313 self._BTN_delete.Enable(False)
314 self._BTN_add.Enable(False)
315 self._BTN_reassign.Enable(False)
316
317 self._LCTRL_doc_type.SetFocus()
318
319
320
322 doc_type = self._LCTRL_doc_type.get_selected_item_data()
323
324 self._TCTRL_type.SetValue(doc_type['type'])
325 self._TCTRL_l10n_type.SetValue(doc_type['l10n_type'])
326
327 self._BTN_set_translation.Enable(True)
328 self._BTN_delete.Enable(not bool(doc_type['is_in_use']))
329 self._BTN_add.Enable(False)
330 self._BTN_reassign.Enable(True)
331
332 return
333
335 self._BTN_set_translation.Enable(False)
336 self._BTN_delete.Enable(False)
337 self._BTN_reassign.Enable(False)
338
339 self._BTN_add.Enable(True)
340
341 return
342
349
366
376
408
410 """Let user select a document type."""
412
413 gmPhraseWheel.cPhraseWheel.__init__(self, *args, **kwargs)
414
415 mp = gmMatchProvider.cMatchProvider_SQL2 (
416 queries = [
417 u"""SELECT
418 data,
419 field_label,
420 list_label
421 FROM ((
422 SELECT
423 pk_doc_type AS data,
424 l10n_type AS field_label,
425 l10n_type AS list_label,
426 1 AS rank
427 FROM blobs.v_doc_type
428 WHERE
429 is_user_defined IS True
430 AND
431 l10n_type %(fragment_condition)s
432 ) UNION (
433 SELECT
434 pk_doc_type AS data,
435 l10n_type AS field_label,
436 l10n_type AS list_label,
437 2 AS rank
438 FROM blobs.v_doc_type
439 WHERE
440 is_user_defined IS False
441 AND
442 l10n_type %(fragment_condition)s
443 )) AS q1
444 ORDER BY q1.rank, q1.list_label"""]
445 )
446 mp.setThresholds(2, 4, 6)
447
448 self.matcher = mp
449 self.picklist_delay = 50
450
451 self.SetToolTipString(_('Select the document type.'))
452
454
455 doc_type = self.GetValue().strip()
456 if doc_type == u'':
457 gmDispatcher.send(signal = u'statustext', msg = _('Cannot create document type without name.'), beep = True)
458 _log.debug('cannot create document type without name')
459 return
460
461 pk = gmDocuments.create_document_type(doc_type)['pk_doc_type']
462 if pk is None:
463 self.data = {}
464 else:
465 self.SetText (
466 value = doc_type,
467 data = pk
468 )
469
470
471
473 if parent is None:
474 parent = wx.GetApp().GetTopWindow()
475 dlg = cReviewDocPartDlg (
476 parent = parent,
477 id = -1,
478 part = part
479 )
480 dlg.ShowModal()
481 dlg.Destroy()
482
485
486 from Gnumed.wxGladeWidgets import wxgReviewDocPartDlg
487
490 """Support parts and docs now.
491 """
492 part = kwds['part']
493 del kwds['part']
494 wxgReviewDocPartDlg.wxgReviewDocPartDlg.__init__(self, *args, **kwds)
495
496 if isinstance(part, gmDocuments.cDocumentPart):
497 self.__part = part
498 self.__doc = self.__part.get_containing_document()
499 self.__reviewing_doc = False
500 elif isinstance(part, gmDocuments.cDocument):
501 self.__doc = part
502 if len(self.__doc.parts) == 0:
503 self.__part = None
504 else:
505 self.__part = self.__doc.parts[0]
506 self.__reviewing_doc = True
507 else:
508 raise ValueError('<part> must be gmDocuments.cDocument or gmDocuments.cDocumentPart instance, got <%s>' % type(part))
509
510 self.__init_ui_data()
511
512
513
515
516
517 self._PhWheel_episode.SetText('%s ' % self.__doc['episode'], self.__doc['pk_episode'])
518 self._PhWheel_doc_type.SetText(value = self.__doc['l10n_type'], data = self.__doc['pk_type'])
519 self._PhWheel_doc_type.add_callback_on_set_focus(self._on_doc_type_gets_focus)
520 self._PhWheel_doc_type.add_callback_on_lose_focus(self._on_doc_type_loses_focus)
521
522 if self.__reviewing_doc:
523 self._PRW_doc_comment.SetText(gmTools.coalesce(self.__doc['comment'], ''))
524 self._PRW_doc_comment.set_context(context = 'pk_doc_type', val = self.__doc['pk_type'])
525 else:
526 self._PRW_doc_comment.SetText(gmTools.coalesce(self.__part['obj_comment'], ''))
527
528 fts = gmDateTime.cFuzzyTimestamp(timestamp = self.__doc['clin_when'])
529 self._PhWheel_doc_date.SetText(fts.strftime('%Y-%m-%d'), fts)
530 self._TCTRL_reference.SetValue(gmTools.coalesce(self.__doc['ext_ref'], ''))
531 if self.__reviewing_doc:
532 self._TCTRL_filename.Enable(False)
533 self._SPINCTRL_seq_idx.Enable(False)
534 else:
535 self._TCTRL_filename.SetValue(gmTools.coalesce(self.__part['filename'], ''))
536 self._SPINCTRL_seq_idx.SetValue(gmTools.coalesce(self.__part['seq_idx'], 0))
537
538 self._LCTRL_existing_reviews.InsertColumn(0, _('who'))
539 self._LCTRL_existing_reviews.InsertColumn(1, _('when'))
540 self._LCTRL_existing_reviews.InsertColumn(2, _('+/-'))
541 self._LCTRL_existing_reviews.InsertColumn(3, _('!'))
542 self._LCTRL_existing_reviews.InsertColumn(4, _('comment'))
543
544 self.__reload_existing_reviews()
545
546 if self._LCTRL_existing_reviews.GetItemCount() > 0:
547 self._LCTRL_existing_reviews.SetColumnWidth(col=0, width=wx.LIST_AUTOSIZE)
548 self._LCTRL_existing_reviews.SetColumnWidth(col=1, width=wx.LIST_AUTOSIZE)
549 self._LCTRL_existing_reviews.SetColumnWidth(col=2, width=wx.LIST_AUTOSIZE_USEHEADER)
550 self._LCTRL_existing_reviews.SetColumnWidth(col=3, width=wx.LIST_AUTOSIZE_USEHEADER)
551 self._LCTRL_existing_reviews.SetColumnWidth(col=4, width=wx.LIST_AUTOSIZE)
552
553 if self.__part is None:
554 self._ChBOX_review.SetValue(False)
555 self._ChBOX_review.Enable(False)
556 self._ChBOX_abnormal.Enable(False)
557 self._ChBOX_relevant.Enable(False)
558 self._ChBOX_sign_all_pages.Enable(False)
559 else:
560 me = gmStaff.gmCurrentProvider()
561 if self.__part['pk_intended_reviewer'] == me['pk_staff']:
562 msg = _('(you are the primary reviewer)')
563 else:
564 other = gmStaff.cStaff(aPK_obj = self.__part['pk_intended_reviewer'])
565 msg = _('(someone else is the intended reviewer: %s)') % other['short_alias']
566 self._TCTRL_responsible.SetValue(msg)
567
568 if self.__part['reviewed_by_you']:
569 revs = self.__part.get_reviews()
570 for rev in revs:
571 if rev['is_your_review']:
572 self._ChBOX_abnormal.SetValue(bool(rev[2]))
573 self._ChBOX_relevant.SetValue(bool(rev[3]))
574 break
575
576 self._ChBOX_sign_all_pages.SetValue(self.__reviewing_doc)
577
578 return True
579
581 self._LCTRL_existing_reviews.DeleteAllItems()
582 if self.__part is None:
583 return True
584 revs = self.__part.get_reviews()
585 if len(revs) == 0:
586 return True
587
588 review_by_responsible_doc = None
589 reviews_by_others = []
590 for rev in revs:
591 if rev['is_review_by_responsible_reviewer'] and not rev['is_your_review']:
592 review_by_responsible_doc = rev
593 if not (rev['is_review_by_responsible_reviewer'] or rev['is_your_review']):
594 reviews_by_others.append(rev)
595
596 if review_by_responsible_doc is not None:
597 row_num = self._LCTRL_existing_reviews.InsertStringItem(sys.maxint, label=review_by_responsible_doc[0])
598 self._LCTRL_existing_reviews.SetItemTextColour(row_num, col=wx.BLUE)
599 self._LCTRL_existing_reviews.SetStringItem(index = row_num, col=0, label=review_by_responsible_doc[0])
600 self._LCTRL_existing_reviews.SetStringItem(index = row_num, col=1, label=review_by_responsible_doc[1].strftime('%x %H:%M'))
601 if review_by_responsible_doc['is_technically_abnormal']:
602 self._LCTRL_existing_reviews.SetStringItem(index = row_num, col=2, label=u'X')
603 if review_by_responsible_doc['clinically_relevant']:
604 self._LCTRL_existing_reviews.SetStringItem(index = row_num, col=3, label=u'X')
605 self._LCTRL_existing_reviews.SetStringItem(index = row_num, col=4, label=review_by_responsible_doc[6])
606 row_num += 1
607 for rev in reviews_by_others:
608 row_num = self._LCTRL_existing_reviews.InsertStringItem(sys.maxint, label=rev[0])
609 self._LCTRL_existing_reviews.SetStringItem(index = row_num, col=0, label=rev[0])
610 self._LCTRL_existing_reviews.SetStringItem(index = row_num, col=1, label=rev[1].strftime('%x %H:%M'))
611 if rev['is_technically_abnormal']:
612 self._LCTRL_existing_reviews.SetStringItem(index = row_num, col=2, label=u'X')
613 if rev['clinically_relevant']:
614 self._LCTRL_existing_reviews.SetStringItem(index = row_num, col=3, label=u'X')
615 self._LCTRL_existing_reviews.SetStringItem(index = row_num, col=4, label=rev[6])
616 return True
617
618
619
710
712 state = self._ChBOX_review.GetValue()
713 self._ChBOX_abnormal.Enable(enable = state)
714 self._ChBOX_relevant.Enable(enable = state)
715 self._ChBOX_responsible.Enable(enable = state)
716
718 """Per Jim: Changing the doc type happens a lot more often
719 then correcting spelling, hence select-all on getting focus.
720 """
721 self._PhWheel_doc_type.SetSelection(-1, -1)
722
724 pk_doc_type = self._PhWheel_doc_type.GetData()
725 if pk_doc_type is None:
726 self._PRW_doc_comment.unset_context(context = 'pk_doc_type')
727 else:
728 self._PRW_doc_comment.set_context(context = 'pk_doc_type', val = pk_doc_type)
729 return True
730
732
733 _log.debug('acquiring images from [%s]', device)
734
735
736
737 from Gnumed.pycommon import gmScanBackend
738 try:
739 fnames = gmScanBackend.acquire_pages_into_files (
740 device = device,
741 delay = 5,
742 calling_window = calling_window
743 )
744 except OSError:
745 _log.exception('problem acquiring image from source')
746 gmGuiHelpers.gm_show_error (
747 aMessage = _(
748 'No images could be acquired from the source.\n\n'
749 'This may mean the scanner driver is not properly installed.\n\n'
750 'On Windows you must install the TWAIN Python module\n'
751 'while on Linux and MacOSX it is recommended to install\n'
752 'the XSane package.'
753 ),
754 aTitle = _('Acquiring images')
755 )
756 return None
757
758 _log.debug('acquired %s images', len(fnames))
759
760 return fnames
761
762 from Gnumed.wxGladeWidgets import wxgScanIdxPnl
763
764 -class cScanIdxDocsPnl(wxgScanIdxPnl.wxgScanIdxPnl, gmPlugin.cPatientChange_PluginMixin):
785
786
787
790
792 pat = gmPerson.gmCurrentPatient()
793 if not pat.connected:
794 gmDispatcher.send(signal='statustext', msg=_('Cannot accept new documents. No active patient.'))
795 return
796
797
798 real_filenames = []
799 for pathname in filenames:
800 try:
801 files = os.listdir(pathname)
802 gmDispatcher.send(signal='statustext', msg=_('Extracting files from folder [%s] ...') % pathname)
803 for file in files:
804 fullname = os.path.join(pathname, file)
805 if not os.path.isfile(fullname):
806 continue
807 real_filenames.append(fullname)
808 except OSError:
809 real_filenames.append(pathname)
810
811 self.acquired_pages.extend(real_filenames)
812 self.__reload_LBOX_doc_pages()
813
816
817
818
822
823 - def _post_patient_selection(self, **kwds):
824 self.__init_ui_data()
825
826
827
829
830 self._PhWheel_episode.SetText(value = _('other documents'), suppress_smarts = True)
831 self._PhWheel_doc_type.SetText('')
832
833
834 fts = gmDateTime.cFuzzyTimestamp()
835 self._PhWheel_doc_date.SetText(fts.strftime('%Y-%m-%d'), fts)
836 self._PRW_doc_comment.SetText('')
837
838 self._PhWheel_reviewer.selection_only = True
839 me = gmStaff.gmCurrentProvider()
840 self._PhWheel_reviewer.SetText (
841 value = u'%s (%s%s %s)' % (me['short_alias'], me['title'], me['firstnames'], me['lastnames']),
842 data = me['pk_staff']
843 )
844
845
846 self._ChBOX_reviewed.SetValue(False)
847 self._ChBOX_abnormal.Disable()
848 self._ChBOX_abnormal.SetValue(False)
849 self._ChBOX_relevant.Disable()
850 self._ChBOX_relevant.SetValue(False)
851
852 self._TBOX_description.SetValue('')
853
854
855 self._LBOX_doc_pages.Clear()
856 self.acquired_pages = []
857
858 self._PhWheel_doc_type.SetFocus()
859
861 self._LBOX_doc_pages.Clear()
862 if len(self.acquired_pages) > 0:
863 for i in range(len(self.acquired_pages)):
864 fname = self.acquired_pages[i]
865 self._LBOX_doc_pages.Append(_('part %s: %s') % (i+1, fname), fname)
866
868 title = _('saving document')
869
870 if self.acquired_pages is None or len(self.acquired_pages) == 0:
871 dbcfg = gmCfg.cCfgSQL()
872 allow_empty = bool(dbcfg.get2 (
873 option = u'horstspace.scan_index.allow_partless_documents',
874 workplace = gmSurgery.gmCurrentPractice().active_workplace,
875 bias = 'user',
876 default = False
877 ))
878 if allow_empty:
879 save_empty = gmGuiHelpers.gm_show_question (
880 aMessage = _('No parts to save. Really save an empty document as a reference ?'),
881 aTitle = title
882 )
883 if not save_empty:
884 return False
885 else:
886 gmGuiHelpers.gm_show_error (
887 aMessage = _('No parts to save. Aquire some parts first.'),
888 aTitle = title
889 )
890 return False
891
892 doc_type_pk = self._PhWheel_doc_type.GetData(can_create = True)
893 if doc_type_pk is None:
894 gmGuiHelpers.gm_show_error (
895 aMessage = _('No document type applied. Choose a document type'),
896 aTitle = title
897 )
898 return False
899
900
901
902
903
904
905
906
907
908 if self._PhWheel_episode.GetValue().strip() == '':
909 gmGuiHelpers.gm_show_error (
910 aMessage = _('You must select an episode to save this document under.'),
911 aTitle = title
912 )
913 return False
914
915 if self._PhWheel_reviewer.GetData() is None:
916 gmGuiHelpers.gm_show_error (
917 aMessage = _('You need to select from the list of staff members the doctor who is intended to sign the document.'),
918 aTitle = title
919 )
920 return False
921
922 return True
923
925
926 if not reconfigure:
927 dbcfg = gmCfg.cCfgSQL()
928 device = dbcfg.get2 (
929 option = 'external.xsane.default_device',
930 workplace = gmSurgery.gmCurrentPractice().active_workplace,
931 bias = 'workplace',
932 default = ''
933 )
934 if device.strip() == u'':
935 device = None
936 if device is not None:
937 return device
938
939 try:
940 devices = self.scan_module.get_devices()
941 except:
942 _log.exception('cannot retrieve list of image sources')
943 gmDispatcher.send(signal = 'statustext', msg = _('There is no scanner support installed on this machine.'))
944 return None
945
946 if devices is None:
947
948
949 return None
950
951 if len(devices) == 0:
952 gmDispatcher.send(signal = 'statustext', msg = _('Cannot find an active scanner.'))
953 return None
954
955
956
957
958
959 device = gmListWidgets.get_choices_from_list (
960 parent = self,
961 msg = _('Select an image capture device'),
962 caption = _('device selection'),
963 choices = [ '%s (%s)' % (d[2], d[0]) for d in devices ],
964 columns = [_('Device')],
965 data = devices,
966 single_selection = True
967 )
968 if device is None:
969 return None
970
971
972 return device[0]
973
974
975
977
978 chosen_device = self.get_device_to_use()
979
980
981
982 try:
983 fnames = self.scan_module.acquire_pages_into_files (
984 device = chosen_device,
985 delay = 5,
986 calling_window = self
987 )
988 except OSError:
989 _log.exception('problem acquiring image from source')
990 gmGuiHelpers.gm_show_error (
991 aMessage = _(
992 'No pages could be acquired from the source.\n\n'
993 'This may mean the scanner driver is not properly installed.\n\n'
994 'On Windows you must install the TWAIN Python module\n'
995 'while on Linux and MacOSX it is recommended to install\n'
996 'the XSane package.'
997 ),
998 aTitle = _('acquiring page')
999 )
1000 return None
1001
1002 if len(fnames) == 0:
1003 return True
1004
1005 self.acquired_pages.extend(fnames)
1006 self.__reload_LBOX_doc_pages()
1007
1008 return True
1009
1011
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
1029
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
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
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
1060 self.acquired_pages[page_idx:(page_idx+1)] = []
1061
1062
1063 self.__reload_LBOX_doc_pages()
1064
1065
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
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
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
1135
1136 new_doc['clin_when'] = self._PhWheel_doc_date.GetData().get_pydt()
1137
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
1153 comment = self._PRW_doc_comment.GetLineText(0).strip()
1154 if comment != u'':
1155 new_doc['comment'] = comment
1156
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
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
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
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
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
1232 self.__init_ui_data()
1233
1235 self._ChBOX_abnormal.Enable(enable = self._ChBOX_reviewed.GetValue())
1236 self._ChBOX_relevant.Enable(enable = self._ChBOX_reviewed.GetValue())
1237
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
1247
1248 if parent is None:
1249 parent = wx.GetApp().GetTopWindow()
1250
1251
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
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
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
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
1295
1296
1297
1298
1299
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:
1307 review_document_part(parent = parent, part = part)
1308 elif review_after_display == 2:
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
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
1332
1333 def delete(document):
1334 return
1335
1336
1337
1338
1339
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
1362
1363
1364 refresh_callback = refresh
1365
1366 )
1367
1368 from Gnumed.wxGladeWidgets import wxgSelectablySortedDocTreePnl
1369
1371 """A panel with a document tree which can be sorted."""
1372
1373
1374
1379
1384
1389
1394
1399
1400 -class cDocTree(wx.TreeCtrl, gmRegetMixin.cRegetOnPaintMixin):
1401
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
1437
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
1450
1452 return self.__sort_mode
1453
1471
1472 sort_mode = property(_get_sort_mode, _set_sort_mode)
1473
1474
1475
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
1488
1490
1491 wx.EVT_TREE_ITEM_ACTIVATED (self, self.GetId(), self._on_activate)
1492 wx.EVT_TREE_ITEM_RIGHT_CLICK (self, self.GetId(), self.__on_right_click)
1493 wx.EVT_TREE_ITEM_GETTOOLTIP(self, -1, self._on_tree_item_gettooltip)
1494
1495
1496
1497 gmDispatcher.connect(signal = u'pre_patient_selection', receiver = self._on_pre_patient_selection)
1498 gmDispatcher.connect(signal = u'post_patient_selection', receiver = self._on_post_patient_selection)
1499 gmDispatcher.connect(signal = u'doc_mod_db', receiver = self._on_doc_mod_db)
1500 gmDispatcher.connect(signal = u'doc_page_mod_db', receiver = self._on_doc_page_mod_db)
1501
1503
1504
1505 self.__part_context_menu = wx.Menu(title = _('Part Actions:'))
1506
1507 ID = wx.NewId()
1508 self.__part_context_menu.Append(ID, _('Display part'))
1509 wx.EVT_MENU(self.__part_context_menu, ID, self.__display_curr_part)
1510
1511 ID = wx.NewId()
1512 self.__part_context_menu.Append(ID, _('%s Sign/Edit properties') % u'\u270D')
1513 wx.EVT_MENU(self.__part_context_menu, ID, self.__review_curr_part)
1514
1515 self.__part_context_menu.AppendSeparator()
1516
1517 item = self.__part_context_menu.Append(-1, _('Delete part'))
1518 self.Bind(wx.EVT_MENU, self.__delete_part, item)
1519
1520 item = self.__part_context_menu.Append(-1, _('Move part'))
1521 self.Bind(wx.EVT_MENU, self.__move_part, item)
1522
1523 ID = wx.NewId()
1524 self.__part_context_menu.Append(ID, _('Print part'))
1525 wx.EVT_MENU(self.__part_context_menu, ID, self.__print_part)
1526
1527 ID = wx.NewId()
1528 self.__part_context_menu.Append(ID, _('Fax part'))
1529 wx.EVT_MENU(self.__part_context_menu, ID, self.__fax_part)
1530
1531 ID = wx.NewId()
1532 self.__part_context_menu.Append(ID, _('Mail part'))
1533 wx.EVT_MENU(self.__part_context_menu, ID, self.__mail_part)
1534
1535 self.__part_context_menu.AppendSeparator()
1536
1537
1538 self.__doc_context_menu = wx.Menu(title = _('Document Actions:'))
1539
1540 ID = wx.NewId()
1541 self.__doc_context_menu.Append(ID, _('%s Sign/Edit properties') % u'\u270D')
1542 wx.EVT_MENU(self.__doc_context_menu, ID, self.__review_curr_part)
1543
1544 self.__doc_context_menu.AppendSeparator()
1545
1546 item = self.__doc_context_menu.Append(-1, _('Add parts'))
1547 self.Bind(wx.EVT_MENU, self.__add_part, item)
1548
1549 ID = wx.NewId()
1550 self.__doc_context_menu.Append(ID, _('Print all parts'))
1551 wx.EVT_MENU(self.__doc_context_menu, ID, self.__print_doc)
1552
1553 ID = wx.NewId()
1554 self.__doc_context_menu.Append(ID, _('Fax all parts'))
1555 wx.EVT_MENU(self.__doc_context_menu, ID, self.__fax_doc)
1556
1557 ID = wx.NewId()
1558 self.__doc_context_menu.Append(ID, _('Mail all parts'))
1559 wx.EVT_MENU(self.__doc_context_menu, ID, self.__mail_doc)
1560
1561 ID = wx.NewId()
1562 self.__doc_context_menu.Append(ID, _('Export all parts'))
1563 wx.EVT_MENU(self.__doc_context_menu, ID, self.__export_doc_to_disk)
1564
1565 self.__doc_context_menu.AppendSeparator()
1566
1567 ID = wx.NewId()
1568 self.__doc_context_menu.Append(ID, _('Delete document'))
1569 wx.EVT_MENU(self.__doc_context_menu, ID, self.__delete_document)
1570
1571 ID = wx.NewId()
1572 self.__doc_context_menu.Append(ID, _('Access external original'))
1573 wx.EVT_MENU(self.__doc_context_menu, ID, self.__access_external_original)
1574
1575 ID = wx.NewId()
1576 self.__doc_context_menu.Append(ID, _('Edit corresponding encounter'))
1577 wx.EVT_MENU(self.__doc_context_menu, ID, self.__edit_encounter_details)
1578
1579 ID = wx.NewId()
1580 self.__doc_context_menu.Append(ID, _('Select corresponding encounter'))
1581 wx.EVT_MENU(self.__doc_context_menu, ID, self.__select_encounter)
1582
1583
1584
1585 ID = wx.NewId()
1586 self.__doc_context_menu.Append(ID, _('Manage descriptions'))
1587 wx.EVT_MENU(self.__doc_context_menu, ID, self.__manage_document_descriptions)
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1605
1606 wx.BeginBusyCursor()
1607
1608
1609 if self.root is not None:
1610 self.DeleteAllItems()
1611
1612
1613 self.root = self.AddRoot(cDocTree._root_node_labels[self.__sort_mode], -1, -1)
1614 self.SetItemPyData(self.root, None)
1615 self.SetItemHasChildren(self.root, False)
1616
1617
1618 curr_pat = gmPerson.gmCurrentPatient()
1619 docs_folder = curr_pat.get_document_folder()
1620 docs = docs_folder.get_documents()
1621
1622 if docs is None:
1623 gmGuiHelpers.gm_show_error (
1624 aMessage = _('Error searching documents.'),
1625 aTitle = _('loading document list')
1626 )
1627
1628 wx.EndBusyCursor()
1629 return True
1630
1631 if len(docs) == 0:
1632 wx.EndBusyCursor()
1633 return True
1634
1635
1636 self.SetItemHasChildren(self.root, True)
1637
1638
1639 intermediate_nodes = {}
1640 for doc in docs:
1641
1642 parts = doc.parts
1643
1644 if len(parts) == 0:
1645 no_parts = _('no parts')
1646 elif len(parts) == 1:
1647 no_parts = _('1 part')
1648 else:
1649 no_parts = _('%s parts') % len(parts)
1650
1651
1652 if self.__sort_mode == 'episode':
1653 inter_label = u'%s%s' % (doc['episode'], gmTools.coalesce(doc['health_issue'], u'', u' (%s)'))
1654 doc_label = _('%s%7s %s:%s (%s)') % (
1655 gmTools.bool2subst(doc.has_unreviewed_parts, gmTools.u_writing_hand, u'', u'?'),
1656 doc['clin_when'].strftime('%m/%Y'),
1657 doc['l10n_type'][:26],
1658 gmTools.coalesce(initial = doc['comment'], instead = u'', template_initial = u' %s'),
1659 no_parts
1660 )
1661 if not intermediate_nodes.has_key(inter_label):
1662 intermediate_nodes[inter_label] = self.AppendItem(parent = self.root, text = inter_label)
1663 self.SetItemBold(intermediate_nodes[inter_label], bold = True)
1664 self.SetItemPyData(intermediate_nodes[inter_label], None)
1665 self.SetItemHasChildren(intermediate_nodes[inter_label], True)
1666 parent = intermediate_nodes[inter_label]
1667
1668 elif self.__sort_mode == 'type':
1669 inter_label = doc['l10n_type']
1670 doc_label = _('%s%7s (%s):%s (%s)') % (
1671 gmTools.bool2subst(doc.has_unreviewed_parts, gmTools.u_writing_hand, u'', u'?'),
1672 doc['clin_when'].strftime('%m/%Y'),
1673 no_parts,
1674 gmTools.coalesce(initial = doc['comment'], instead = u'', template_initial = u' %s'),
1675 u'%s%s' % (doc['episode'], gmTools.coalesce(doc['health_issue'], u'', u' %s %%s' % gmTools.u_right_arrow))
1676 )
1677 if not intermediate_nodes.has_key(inter_label):
1678 intermediate_nodes[inter_label] = self.AppendItem(parent = self.root, text = inter_label)
1679 self.SetItemBold(intermediate_nodes[inter_label], bold = True)
1680 self.SetItemPyData(intermediate_nodes[inter_label], None)
1681 self.SetItemHasChildren(intermediate_nodes[inter_label], True)
1682 parent = intermediate_nodes[inter_label]
1683
1684 elif self.__sort_mode == 'issue':
1685 if doc['health_issue'] is None:
1686 inter_label = _('Unattributed episode: %s') % doc['episode']
1687 else:
1688 inter_label = doc['health_issue']
1689 doc_label = _('%s%7s %s:%s (%s)') % (
1690 gmTools.bool2subst(doc.has_unreviewed_parts, gmTools.u_writing_hand, u'', u'?'),
1691 doc['clin_when'].strftime('%m/%Y'),
1692 doc['l10n_type'][:26],
1693 gmTools.coalesce(initial = doc['comment'], instead = u'', template_initial = u' %s'),
1694 no_parts
1695 )
1696 if not intermediate_nodes.has_key(inter_label):
1697 intermediate_nodes[inter_label] = self.AppendItem(parent = self.root, text = inter_label)
1698 self.SetItemBold(intermediate_nodes[inter_label], bold = True)
1699 self.SetItemPyData(intermediate_nodes[inter_label], None)
1700 self.SetItemHasChildren(intermediate_nodes[inter_label], True)
1701 parent = intermediate_nodes[inter_label]
1702
1703 else:
1704 doc_label = _('%s%7s %s:%s (%s)') % (
1705 gmTools.bool2subst(doc.has_unreviewed_parts, gmTools.u_writing_hand, u'', u'?'),
1706 doc['clin_when'].strftime('%m/%Y'),
1707 doc['l10n_type'][:26],
1708 gmTools.coalesce(initial = doc['comment'], instead = u'', template_initial = u' %s'),
1709 no_parts
1710 )
1711 parent = self.root
1712
1713 doc_node = self.AppendItem(parent = parent, text = doc_label)
1714
1715 self.SetItemPyData(doc_node, doc)
1716 if len(parts) == 0:
1717 self.SetItemHasChildren(doc_node, False)
1718 else:
1719 self.SetItemHasChildren(doc_node, True)
1720
1721
1722 for part in parts:
1723
1724
1725
1726
1727 f_ext = u''
1728 if part['filename'] is not None:
1729 f_ext = os.path.splitext(part['filename'])[1].strip('.').strip()
1730 if f_ext != u'':
1731 f_ext = u' .' + f_ext.upper()
1732 label = '%s%s (%s%s)%s' % (
1733 gmTools.bool2str (
1734 boolean = part['reviewed'] or part['reviewed_by_you'] or part['reviewed_by_intended_reviewer'],
1735 true_str = u'',
1736 false_str = gmTools.u_writing_hand
1737 ),
1738 _('part %2s') % part['seq_idx'],
1739 gmTools.size2str(part['size']),
1740 f_ext,
1741 gmTools.coalesce (
1742 part['obj_comment'],
1743 u'',
1744 u': %s%%s%s' % (gmTools.u_left_double_angle_quote, gmTools.u_right_double_angle_quote)
1745 )
1746 )
1747
1748 part_node = self.AppendItem(parent = doc_node, text = label)
1749 self.SetItemPyData(part_node, part)
1750 self.SetItemHasChildren(part_node, False)
1751
1752 self.__sort_nodes()
1753 self.SelectItem(self.root)
1754
1755
1756
1757 self.Expand(self.root)
1758 if self.__sort_mode in ['episode', 'type', 'issue']:
1759 for key in intermediate_nodes.keys():
1760 self.Expand(intermediate_nodes[key])
1761
1762 wx.EndBusyCursor()
1763
1764 return True
1765
1767 """Used in sorting items.
1768
1769 -1: 1 < 2
1770 0: 1 = 2
1771 1: 1 > 2
1772 """
1773
1774 if not node1:
1775 _log.debug('invalid node 1')
1776 return 0
1777 if not node2:
1778 _log.debug('invalid node 2')
1779 return 0
1780 if not node1.IsOk():
1781 _log.debug('no data on node 1')
1782 return 0
1783 if not node2.IsOk():
1784 _log.debug('no data on node 2')
1785 return 0
1786
1787 data1 = self.GetPyData(node1)
1788 data2 = self.GetPyData(node2)
1789
1790
1791 if isinstance(data1, gmDocuments.cDocument):
1792
1793 date_field = 'clin_when'
1794
1795
1796 if self.__sort_mode == 'age':
1797
1798 if data1[date_field] > data2[date_field]:
1799 return -1
1800 if data1[date_field] == data2[date_field]:
1801 return 0
1802 return 1
1803
1804 elif self.__sort_mode == 'episode':
1805 if data1['episode'] < data2['episode']:
1806 return -1
1807 if data1['episode'] == data2['episode']:
1808
1809 if data1[date_field] > data2[date_field]:
1810 return -1
1811 if data1[date_field] == data2[date_field]:
1812 return 0
1813 return 1
1814 return 1
1815
1816 elif self.__sort_mode == 'issue':
1817 if data1['health_issue'] < data2['health_issue']:
1818 return -1
1819 if data1['health_issue'] == data2['health_issue']:
1820
1821 if data1[date_field] > data2[date_field]:
1822 return -1
1823 if data1[date_field] == data2[date_field]:
1824 return 0
1825 return 1
1826 return 1
1827
1828 elif self.__sort_mode == 'review':
1829
1830 if data1.has_unreviewed_parts == data2.has_unreviewed_parts:
1831
1832 if data1[date_field] > data2[date_field]:
1833 return -1
1834 if data1[date_field] == data2[date_field]:
1835 return 0
1836 return 1
1837 if data1.has_unreviewed_parts:
1838 return -1
1839 return 1
1840
1841 elif self.__sort_mode == 'type':
1842 if data1['l10n_type'] < data2['l10n_type']:
1843 return -1
1844 if data1['l10n_type'] == data2['l10n_type']:
1845
1846 if data1[date_field] > data2[date_field]:
1847 return -1
1848 if data1[date_field] == data2[date_field]:
1849 return 0
1850 return 1
1851 return 1
1852
1853 else:
1854 _log.error('unknown document sort mode [%s], reverse-sorting by age', self.__sort_mode)
1855
1856 if data1[date_field] > data2[date_field]:
1857 return -1
1858 if data1[date_field] == data2[date_field]:
1859 return 0
1860 return 1
1861
1862
1863 if isinstance(data1, gmDocuments.cDocumentPart):
1864
1865
1866 if data1['seq_idx'] < data2['seq_idx']:
1867 return -1
1868 if data1['seq_idx'] == data2['seq_idx']:
1869 return 0
1870 return 1
1871
1872
1873 if None in [data1, data2]:
1874 l1 = self.GetItemText(node1)
1875 l2 = self.GetItemText(node2)
1876 if l1 < l2:
1877 return -1
1878 if l1 == l2:
1879 return 0
1880 else:
1881 if data1 < data2:
1882 return -1
1883 if data1 == data2:
1884 return 0
1885 return 1
1886
1887
1888
1890
1891 wx.CallAfter(self._schedule_data_reget)
1892
1893 - def _on_doc_page_mod_db(self, *args, **kwargs):
1894
1895 wx.CallAfter(self._schedule_data_reget)
1896
1898
1899
1900
1901 if self.root is not None:
1902 self.DeleteAllItems()
1903 self.root = None
1904
1905 - def _on_post_patient_selection(self, *args, **kwargs):
1906
1907 self._schedule_data_reget()
1908
1910 node = event.GetItem()
1911 node_data = self.GetPyData(node)
1912
1913
1914 if node_data is None:
1915 return None
1916
1917
1918 if isinstance(node_data, gmDocuments.cDocument):
1919 self.Toggle(node)
1920 return True
1921
1922
1923 if type(node_data) == type('string'):
1924 self.Toggle(node)
1925 return True
1926
1927 self.__display_part(part = node_data)
1928 return True
1929
1931
1932 node = evt.GetItem()
1933 self.__curr_node_data = self.GetPyData(node)
1934
1935
1936 if self.__curr_node_data is None:
1937 return None
1938
1939
1940 if isinstance(self.__curr_node_data, gmDocuments.cDocument):
1941 self.__handle_doc_context()
1942
1943
1944 if isinstance(self.__curr_node_data, gmDocuments.cDocumentPart):
1945 self.__handle_part_context()
1946
1947 del self.__curr_node_data
1948 evt.Skip()
1949
1952
1954 self.__display_part(part = self.__curr_node_data)
1955
1957 self.__review_part(part = self.__curr_node_data)
1958
1961
1983
1984
1985
1987
1988 if start_node is None:
1989 start_node = self.GetRootItem()
1990
1991
1992
1993 if not start_node.IsOk():
1994 return True
1995
1996 self.SortChildren(start_node)
1997
1998 child_node, cookie = self.GetFirstChild(start_node)
1999 while child_node.IsOk():
2000 self.__sort_nodes(start_node = child_node)
2001 child_node, cookie = self.GetNextChild(start_node, cookie)
2002
2003 return
2004
2006 self.PopupMenu(self.__doc_context_menu, wx.DefaultPosition)
2007
2009
2010 if self.__curr_node_data['type'] == 'patient photograph':
2011 ID = wx.NewId()
2012 self.__part_context_menu.Append(ID, _('Activate as current photo'))
2013 wx.EVT_MENU(self.__part_context_menu, ID, self.__activate_as_current_photo)
2014 else:
2015 ID = None
2016
2017 self.PopupMenu(self.__part_context_menu, wx.DefaultPosition)
2018
2019 if ID is not None:
2020 self.__part_context_menu.Delete(ID)
2021
2022
2023
2025 """Display document part."""
2026
2027
2028 if part['size'] == 0:
2029 _log.debug('cannot display part [%s] - 0 bytes', part['pk_obj'])
2030 gmGuiHelpers.gm_show_error (
2031 aMessage = _('Document part does not seem to exist in database !'),
2032 aTitle = _('showing document')
2033 )
2034 return None
2035
2036 wx.BeginBusyCursor()
2037
2038 cfg = gmCfg.cCfgSQL()
2039
2040
2041 chunksize = int(
2042 cfg.get2 (
2043 option = "horstspace.blob_export_chunk_size",
2044 workplace = gmSurgery.gmCurrentPractice().active_workplace,
2045 bias = 'workplace',
2046 default = default_chunksize
2047 ))
2048
2049
2050 block_during_view = bool( cfg.get2 (
2051 option = 'horstspace.document_viewer.block_during_view',
2052 workplace = gmSurgery.gmCurrentPractice().active_workplace,
2053 bias = 'user',
2054 default = None
2055 ))
2056
2057
2058 successful, msg = part.display_via_mime (
2059 chunksize = chunksize,
2060 block = block_during_view
2061 )
2062
2063 wx.EndBusyCursor()
2064
2065 if not successful:
2066 gmGuiHelpers.gm_show_error (
2067 aMessage = _('Cannot display document part:\n%s') % msg,
2068 aTitle = _('showing document')
2069 )
2070 return None
2071
2072
2073
2074
2075
2076
2077
2078 review_after_display = int(cfg.get2 (
2079 option = 'horstspace.document_viewer.review_after_display',
2080 workplace = gmSurgery.gmCurrentPractice().active_workplace,
2081 bias = 'user',
2082 default = 3
2083 ))
2084 if review_after_display == 1:
2085 self.__review_part(part=part)
2086 elif review_after_display == 2:
2087 review_by_me = filter(lambda rev: rev['is_your_review'], part.get_reviews())
2088 if len(review_by_me) == 0:
2089 self.__review_part(part = part)
2090 elif review_after_display == 3:
2091 if len(part.get_reviews()) == 0:
2092 self.__review_part(part = part)
2093 elif review_after_display == 4:
2094 reviewed_by_responsible = filter(lambda rev: rev['is_review_by_responsible_reviewer'], part.get_reviews())
2095 if len(reviewed_by_responsible) == 0:
2096 self.__review_part(part = part)
2097
2098 return True
2099
2101 dlg = cReviewDocPartDlg (
2102 parent = self,
2103 id = -1,
2104 part = part
2105 )
2106 dlg.ShowModal()
2107 dlg.Destroy()
2108
2110 target_doc = manage_documents (
2111 parent = self,
2112 msg = _('\nSelect the document into which to move the selected part !\n')
2113 )
2114 if target_doc is None:
2115 return
2116 self.__curr_node_data['pk_doc'] = target_doc['pk_doc']
2117 self.__curr_node_data.save()
2118
2120 delete_it = gmGuiHelpers.gm_show_question (
2121 cancel_button = True,
2122 title = _('Deleting document part'),
2123 question = _(
2124 'Are you sure you want to delete the %s part #%s\n'
2125 '\n'
2126 '%s'
2127 'from the following document\n'
2128 '\n'
2129 ' %s (%s)\n'
2130 '%s'
2131 '\n'
2132 'Really delete ?\n'
2133 '\n'
2134 '(this action cannot be reversed)'
2135 ) % (
2136 gmTools.size2str(self.__curr_node_data['size']),
2137 self.__curr_node_data['seq_idx'],
2138 gmTools.coalesce(self.__curr_node_data['obj_comment'], u'', u' "%s"\n\n'),
2139 self.__curr_node_data['l10n_type'],
2140 gmDateTime.pydt_strftime(self.__curr_node_data['date_generated'], format = '%Y-%m-%d', accuracy = gmDateTime.acc_days),
2141 gmTools.coalesce(self.__curr_node_data['doc_comment'], u'', u' "%s"\n')
2142 )
2143 )
2144 if not delete_it:
2145 return
2146
2147 gmDocuments.delete_document_part (
2148 part_pk = self.__curr_node_data['pk_obj'],
2149 encounter_pk = gmPerson.gmCurrentPatient().emr.active_encounter['pk_encounter']
2150 )
2151
2153
2154 gmHooks.run_hook_script(hook = u'before_%s_doc_part' % action)
2155
2156 wx.BeginBusyCursor()
2157
2158
2159 found, external_cmd = gmShellAPI.detect_external_binary(u'gm-%s_doc' % action)
2160 if not found:
2161 found, external_cmd = gmShellAPI.detect_external_binary(u'gm-%s_doc.bat' % action)
2162 if not found:
2163 _log.error('neither of gm-%s_doc or gm-%s_doc.bat found', action, action)
2164 wx.EndBusyCursor()
2165 gmGuiHelpers.gm_show_error (
2166 _('Cannot %(l10n_action)s document part - %(l10n_action)s command not found.\n'
2167 '\n'
2168 'Either of gm_%(action)s_doc.sh or gm_%(action)s_doc.bat\n'
2169 'must be in the execution path. The command will\n'
2170 'be passed the filename to %(l10n_action)s.'
2171 ) % {'action': action, 'l10n_action': l10n_action},
2172 _('Processing document part: %s') % l10n_action
2173 )
2174 return
2175
2176 cfg = gmCfg.cCfgSQL()
2177
2178
2179 chunksize = int(cfg.get2 (
2180 option = "horstspace.blob_export_chunk_size",
2181 workplace = gmSurgery.gmCurrentPractice().active_workplace,
2182 bias = 'workplace',
2183 default = default_chunksize
2184 ))
2185
2186 part_file = self.__curr_node_data.export_to_file(aChunkSize = chunksize)
2187
2188 cmd = u'%s %s' % (external_cmd, part_file)
2189 if os.name == 'nt':
2190 blocking = True
2191 else:
2192 blocking = False
2193 success = gmShellAPI.run_command_in_shell (
2194 command = cmd,
2195 blocking = blocking
2196 )
2197
2198 wx.EndBusyCursor()
2199
2200 if not success:
2201 _log.error('%s command failed: [%s]', action, cmd)
2202 gmGuiHelpers.gm_show_error (
2203 _('Cannot %(l10n_action)s document part - %(l10n_action)s command failed.\n'
2204 '\n'
2205 'You may need to check and fix either of\n'
2206 ' gm_%(action)s_doc.sh (Unix/Mac) or\n'
2207 ' gm_%(action)s_doc.bat (Windows)\n'
2208 '\n'
2209 'The command is passed the filename to %(l10n_action)s.'
2210 ) % {'action': action, 'l10n_action': l10n_action},
2211 _('Processing document part: %s') % l10n_action
2212 )
2213
2215 self.__process_part(action = u'print', l10n_action = _('print'))
2216
2218 self.__process_part(action = u'fax', l10n_action = _('fax'))
2219
2221 self.__process_part(action = u'mail', l10n_action = _('mail'))
2222
2223
2224
2234
2238
2240
2241 gmHooks.run_hook_script(hook = u'before_%s_doc' % action)
2242
2243 wx.BeginBusyCursor()
2244
2245
2246 found, external_cmd = gmShellAPI.detect_external_binary(u'gm-%s_doc' % action)
2247 if not found:
2248 found, external_cmd = gmShellAPI.detect_external_binary(u'gm-%s_doc.bat' % action)
2249 if not found:
2250 _log.error('neither of gm-%s_doc or gm-%s_doc.bat found', action, action)
2251 wx.EndBusyCursor()
2252 gmGuiHelpers.gm_show_error (
2253 _('Cannot %(l10n_action)s document - %(l10n_action)s command not found.\n'
2254 '\n'
2255 'Either of gm_%(action)s_doc.sh or gm_%(action)s_doc.bat\n'
2256 'must be in the execution path. The command will\n'
2257 'be passed a list of filenames to %(l10n_action)s.'
2258 ) % {'action': action, 'l10n_action': l10n_action},
2259 _('Processing document: %s') % l10n_action
2260 )
2261 return
2262
2263 cfg = gmCfg.cCfgSQL()
2264
2265
2266 chunksize = int(cfg.get2 (
2267 option = "horstspace.blob_export_chunk_size",
2268 workplace = gmSurgery.gmCurrentPractice().active_workplace,
2269 bias = 'workplace',
2270 default = default_chunksize
2271 ))
2272
2273 part_files = self.__curr_node_data.export_parts_to_files(chunksize = chunksize)
2274
2275 if os.name == 'nt':
2276 blocking = True
2277 else:
2278 blocking = False
2279 cmd = external_cmd + u' ' + u' '.join(part_files)
2280 success = gmShellAPI.run_command_in_shell (
2281 command = cmd,
2282 blocking = blocking
2283 )
2284
2285 wx.EndBusyCursor()
2286
2287 if not success:
2288 _log.error('%s command failed: [%s]', action, cmd)
2289 gmGuiHelpers.gm_show_error (
2290 _('Cannot %(l10n_action)s document - %(l10n_action)s command failed.\n'
2291 '\n'
2292 'You may need to check and fix either of\n'
2293 ' gm_%(action)s_doc.sh (Unix/Mac) or\n'
2294 ' gm_%(action)s_doc.bat (Windows)\n'
2295 '\n'
2296 'The command is passed a list of filenames to %(l10n_action)s.'
2297 ) % {'action': action, 'l10n_action': l10n_action},
2298 _('Processing document: %s') % l10n_action
2299 )
2300
2301
2303 self.__process_doc(action = u'print', l10n_action = _('print'))
2304
2306 self.__process_doc(action = u'fax', l10n_action = _('fax'))
2307
2309 self.__process_doc(action = u'mail', l10n_action = _('mail'))
2310
2312 dlg = wx.FileDialog (
2313 parent = self,
2314 message = _('Choose a file'),
2315 defaultDir = os.path.expanduser(os.path.join('~', 'gnumed')),
2316 defaultFile = '',
2317 wildcard = "%s (*)|*|PNGs (*.png)|*.png|PDFs (*.pdf)|*.pdf|TIFFs (*.tif)|*.tif|JPEGs (*.jpg)|*.jpg|%s (*.*)|*.*" % (_('all files'), _('all files (Win)')),
2318 style = wx.OPEN | wx.FILE_MUST_EXIST | wx.MULTIPLE
2319 )
2320 result = dlg.ShowModal()
2321 if result != wx.ID_CANCEL:
2322 self.__curr_node_data.add_parts_from_files(files = dlg.GetPaths(), reviewer = gmStaff.gmCurrentProvider()['pk_staff'])
2323 dlg.Destroy()
2324
2326
2327 gmHooks.run_hook_script(hook = u'before_external_doc_access')
2328
2329 wx.BeginBusyCursor()
2330
2331
2332 found, external_cmd = gmShellAPI.detect_external_binary(u'gm_access_external_doc.sh')
2333 if not found:
2334 found, external_cmd = gmShellAPI.detect_external_binary(u'gm_access_external_doc.bat')
2335 if not found:
2336 _log.error('neither of gm_access_external_doc.sh or .bat found')
2337 wx.EndBusyCursor()
2338 gmGuiHelpers.gm_show_error (
2339 _('Cannot access external document - access command not found.\n'
2340 '\n'
2341 'Either of gm_access_external_doc.sh or *.bat must be\n'
2342 'in the execution path. The command will be passed the\n'
2343 'document type and the reference URL for processing.'
2344 ),
2345 _('Accessing external document')
2346 )
2347 return
2348
2349 cmd = u'%s "%s" "%s"' % (external_cmd, self.__curr_node_data['type'], self.__curr_node_data['ext_ref'])
2350 if os.name == 'nt':
2351 blocking = True
2352 else:
2353 blocking = False
2354 success = gmShellAPI.run_command_in_shell (
2355 command = cmd,
2356 blocking = blocking
2357 )
2358
2359 wx.EndBusyCursor()
2360
2361 if not success:
2362 _log.error('External access command failed: [%s]', cmd)
2363 gmGuiHelpers.gm_show_error (
2364 _('Cannot access external document - access command failed.\n'
2365 '\n'
2366 'You may need to check and fix either of\n'
2367 ' gm_access_external_doc.sh (Unix/Mac) or\n'
2368 ' gm_access_external_doc.bat (Windows)\n'
2369 '\n'
2370 'The command is passed the document type and the\n'
2371 'external reference URL on the command line.'
2372 ),
2373 _('Accessing external document')
2374 )
2375
2377 """Export document into directory.
2378
2379 - one file per object
2380 - into subdirectory named after patient
2381 """
2382 pat = gmPerson.gmCurrentPatient()
2383 dname = '%s-%s%s' % (
2384 self.__curr_node_data['l10n_type'],
2385 self.__curr_node_data['clin_when'].strftime('%Y-%m-%d'),
2386 gmTools.coalesce(self.__curr_node_data['ext_ref'], '', '-%s').replace(' ', '_')
2387 )
2388 def_dir = os.path.expanduser(os.path.join('~', 'gnumed', 'export', 'docs', pat['dirname'], dname))
2389 gmTools.mkdir(def_dir)
2390
2391 dlg = wx.DirDialog (
2392 parent = self,
2393 message = _('Save document into directory ...'),
2394 defaultPath = def_dir,
2395 style = wx.DD_DEFAULT_STYLE
2396 )
2397 result = dlg.ShowModal()
2398 dirname = dlg.GetPath()
2399 dlg.Destroy()
2400
2401 if result != wx.ID_OK:
2402 return True
2403
2404 wx.BeginBusyCursor()
2405
2406 cfg = gmCfg.cCfgSQL()
2407
2408
2409 chunksize = int(cfg.get2 (
2410 option = "horstspace.blob_export_chunk_size",
2411 workplace = gmSurgery.gmCurrentPractice().active_workplace,
2412 bias = 'workplace',
2413 default = default_chunksize
2414 ))
2415
2416 fnames = self.__curr_node_data.export_parts_to_files(export_dir = dirname, chunksize = chunksize)
2417
2418 wx.EndBusyCursor()
2419
2420 gmDispatcher.send(signal='statustext', msg=_('Successfully exported %s parts into the directory [%s].') % (len(fnames), dirname))
2421
2422 return True
2423
2434
2435
2436
2437 if __name__ == '__main__':
2438
2439 gmI18N.activate_locale()
2440 gmI18N.install_domain(domain = 'gnumed')
2441
2442
2443
2444 if (len(sys.argv) > 1) and (sys.argv[1] == 'test'):
2445
2446 pass
2447
2448
2449