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