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