1 """GNUmed medical document handling widgets.
2 """
3
4
5
6 __version__ = "$Revision: 1.187 $"
7 __author__ = "Karsten Hilbert <Karsten.Hilbert@gmx.net>"
8
9 import os.path, sys, re as regex, logging
10
11
12 import wx
13
14
15 if __name__ == '__main__':
16 sys.path.insert(0, '../../')
17 from Gnumed.pycommon import gmI18N, gmCfg, gmPG2, gmMimeLib, gmExceptions, gmMatchProvider, gmDispatcher, gmDateTime, gmTools, gmShellAPI, gmHooks
18 from Gnumed.business import gmPerson, gmMedDoc, gmEMRStructItems, gmSurgery
19 from Gnumed.wxpython import gmGuiHelpers, gmRegetMixin, gmPhraseWheel, gmPlugin, gmEMRStructWidgets, gmListWidgets
20 from Gnumed.wxGladeWidgets import wxgScanIdxPnl, wxgReviewDocPartDlg, wxgSelectablySortedDocTreePnl, wxgEditDocumentTypesPnl, wxgEditDocumentTypesDlg
21
22
23 _log = logging.getLogger('gm.ui')
24 _log.info(__version__)
25
26
27 default_chunksize = 1 * 1024 * 1024
28
30
31
32 def delete_item(item):
33 doit = gmGuiHelpers.gm_show_question (
34 _( 'Are you sure you want to delete this\n'
35 'description from the document ?\n'
36 ),
37 _('Deleting document description')
38 )
39 if not doit:
40 return True
41
42 document.delete_description(pk = item[0])
43 return True
44
45 def add_item():
46 dlg = gmGuiHelpers.cMultilineTextEntryDlg (
47 parent,
48 -1,
49 title = _('Adding document description'),
50 msg = _('Below you can add a document description.\n')
51 )
52 result = dlg.ShowModal()
53 if result == wx.ID_SAVE:
54 document.add_description(dlg.value)
55
56 dlg.Destroy()
57 return True
58
59 def edit_item(item):
60 dlg = gmGuiHelpers.cMultilineTextEntryDlg (
61 parent,
62 -1,
63 title = _('Editing document description'),
64 msg = _('Below you can edit the document description.\n'),
65 text = item[1]
66 )
67 result = dlg.ShowModal()
68 if result == wx.ID_SAVE:
69 document.update_description(pk = item[0], description = dlg.value)
70
71 dlg.Destroy()
72 return True
73
74 def refresh_list(lctrl):
75 descriptions = document.get_descriptions()
76
77 lctrl.set_string_items(items = [
78 u'%s%s' % ( (u' '.join(regex.split('\r\n+|\r+|\n+|\t+', desc[1])))[:30], gmTools.u_ellipsis )
79 for desc in descriptions
80 ])
81 lctrl.set_data(data = descriptions)
82
83
84 gmListWidgets.get_choices_from_list (
85 parent = parent,
86 msg = _('Select the description you are interested in.\n'),
87 caption = _('Managing document descriptions'),
88 columns = [_('Description')],
89 edit_callback = edit_item,
90 new_callback = add_item,
91 delete_callback = delete_item,
92 refresh_callback = refresh_list,
93 single_selection = True,
94 can_return_empty = True
95 )
96
97 return True
98
101
145
146 gmDispatcher.connect(signal = u'import_document_from_file', receiver = _save_file_as_new_document)
147
192
194 """A dialog showing a cEditDocumentTypesPnl."""
195
197 wxgEditDocumentTypesDlg.wxgEditDocumentTypesDlg.__init__(self, *args, **kwargs)
198
199
201 """A panel grouping together fields to edit the list of document types."""
202
204 wxgEditDocumentTypesPnl.wxgEditDocumentTypesPnl.__init__(self, *args, **kwargs)
205 self.__init_ui()
206 self.__register_interests()
207 self.repopulate_ui()
208
212
215
218
220
221 self._LCTRL_doc_type.DeleteAllItems()
222
223 doc_types = gmMedDoc.get_document_types()
224 pos = len(doc_types) + 1
225
226 for doc_type in doc_types:
227 row_num = self._LCTRL_doc_type.InsertStringItem(pos, label = doc_type['type'])
228 self._LCTRL_doc_type.SetStringItem(index = row_num, col = 1, label = doc_type['l10n_type'])
229 if doc_type['is_user_defined']:
230 self._LCTRL_doc_type.SetStringItem(index = row_num, col = 2, label = ' X ')
231 if doc_type['is_in_use']:
232 self._LCTRL_doc_type.SetStringItem(index = row_num, col = 3, label = ' X ')
233
234 if len(doc_types) > 0:
235 self._LCTRL_doc_type.set_data(data = doc_types)
236 self._LCTRL_doc_type.SetColumnWidth(col=0, width=wx.LIST_AUTOSIZE)
237 self._LCTRL_doc_type.SetColumnWidth(col=1, width=wx.LIST_AUTOSIZE)
238 self._LCTRL_doc_type.SetColumnWidth(col=2, width=wx.LIST_AUTOSIZE_USEHEADER)
239 self._LCTRL_doc_type.SetColumnWidth(col=3, width=wx.LIST_AUTOSIZE_USEHEADER)
240
241 self._TCTRL_type.SetValue('')
242 self._TCTRL_l10n_type.SetValue('')
243
244 self._BTN_set_translation.Enable(False)
245 self._BTN_delete.Enable(False)
246 self._BTN_add.Enable(False)
247 self._BTN_reassign.Enable(False)
248
249 self._LCTRL_doc_type.SetFocus()
250
251
252
254 doc_type = self._LCTRL_doc_type.get_selected_item_data()
255
256 self._TCTRL_type.SetValue(doc_type['type'])
257 self._TCTRL_l10n_type.SetValue(doc_type['l10n_type'])
258
259 self._BTN_set_translation.Enable(True)
260 self._BTN_delete.Enable(not bool(doc_type['is_in_use']))
261 self._BTN_add.Enable(False)
262 self._BTN_reassign.Enable(True)
263
264 return
265
267 self._BTN_set_translation.Enable(False)
268 self._BTN_delete.Enable(False)
269 self._BTN_reassign.Enable(False)
270
271 self._BTN_add.Enable(True)
272
273 return
274
281
298
308
340
342 """Let user select a document type."""
344
345 gmPhraseWheel.cPhraseWheel.__init__(self, *args, **kwargs)
346
347 mp = gmMatchProvider.cMatchProvider_SQL2 (
348 queries = [
349 u"""select * from ((
350 select pk_doc_type, l10n_type, 1 as rank from blobs.v_doc_type where
351 is_user_defined is True and
352 l10n_type %(fragment_condition)s
353 ) union (
354 select pk_doc_type, l10n_type, 2 from blobs.v_doc_type where
355 is_user_defined is False and
356 l10n_type %(fragment_condition)s
357 )) as q1 order by q1.rank, q1.l10n_type
358 """]
359 )
360 mp.setThresholds(2, 4, 6)
361
362 self.matcher = mp
363 self.picklist_delay = 50
364
365 self.SetToolTipString(_('Select the document type.'))
366
367 - def GetData(self, can_create=False):
372
375 """Support parts and docs now.
376 """
377 part = kwds['part']
378 del kwds['part']
379 wxgReviewDocPartDlg.wxgReviewDocPartDlg.__init__(self, *args, **kwds)
380
381 if isinstance(part, gmMedDoc.cMedDocPart):
382 self.__part = part
383 self.__doc = self.__part.get_containing_document()
384 self.__reviewing_doc = False
385 elif isinstance(part, gmMedDoc.cMedDoc):
386 self.__doc = part
387 self.__part = self.__doc.get_parts()[0]
388 self.__reviewing_doc = True
389 else:
390 raise ValueError('<part> must be gmMedDoc.cMedDoc or gmMedDoc.cMedDocPart instance, got <%s>' % type(part))
391
392 self.__init_ui_data()
393
394
395
397
398
399 self._PhWheel_episode.SetText('%s ' % self.__part['episode'], self.__part['pk_episode'])
400 self._PhWheel_doc_type.SetText(value = self.__part['l10n_type'], data = self.__part['pk_type'])
401 self._PhWheel_doc_type.add_callback_on_set_focus(self._on_doc_type_gets_focus)
402 self._PhWheel_doc_type.add_callback_on_lose_focus(self._on_doc_type_loses_focus)
403
404 if self.__reviewing_doc:
405 self._PRW_doc_comment.SetText(gmTools.coalesce(self.__part['doc_comment'], ''))
406 self._PRW_doc_comment.set_context(context = 'pk_doc_type', val = self.__part['pk_type'])
407 else:
408 self._PRW_doc_comment.SetText(gmTools.coalesce(self.__part['obj_comment'], ''))
409
410 fts = gmDateTime.cFuzzyTimestamp(timestamp = self.__part['date_generated'])
411 self._PhWheel_doc_date.SetText(fts.strftime('%Y-%m-%d'), fts)
412 self._TCTRL_reference.SetValue(gmTools.coalesce(self.__part['ext_ref'], ''))
413 if self.__reviewing_doc:
414 self._TCTRL_filename.Enable(False)
415 self._SPINCTRL_seq_idx.Enable(False)
416 else:
417 self._TCTRL_filename.SetValue(gmTools.coalesce(self.__part['filename'], ''))
418 self._SPINCTRL_seq_idx.SetValue(gmTools.coalesce(self.__part['seq_idx'], 0))
419
420 self._LCTRL_existing_reviews.InsertColumn(0, _('who'))
421 self._LCTRL_existing_reviews.InsertColumn(1, _('when'))
422 self._LCTRL_existing_reviews.InsertColumn(2, _('+/-'))
423 self._LCTRL_existing_reviews.InsertColumn(3, _('!'))
424 self._LCTRL_existing_reviews.InsertColumn(4, _('comment'))
425
426 self.__reload_existing_reviews()
427
428 if self._LCTRL_existing_reviews.GetItemCount() > 0:
429 self._LCTRL_existing_reviews.SetColumnWidth(col=0, width=wx.LIST_AUTOSIZE)
430 self._LCTRL_existing_reviews.SetColumnWidth(col=1, width=wx.LIST_AUTOSIZE)
431 self._LCTRL_existing_reviews.SetColumnWidth(col=2, width=wx.LIST_AUTOSIZE_USEHEADER)
432 self._LCTRL_existing_reviews.SetColumnWidth(col=3, width=wx.LIST_AUTOSIZE_USEHEADER)
433 self._LCTRL_existing_reviews.SetColumnWidth(col=4, width=wx.LIST_AUTOSIZE)
434
435 me = gmPerson.gmCurrentProvider()
436 if self.__part['pk_intended_reviewer'] == me['pk_staff']:
437 msg = _('(you are the primary reviewer)')
438 else:
439 msg = _('(someone else is the primary reviewer)')
440 self._TCTRL_responsible.SetValue(msg)
441
442
443 if self.__part['reviewed_by_you']:
444 revs = self.__part.get_reviews()
445 for rev in revs:
446 if rev['is_your_review']:
447 self._ChBOX_abnormal.SetValue(bool(rev[2]))
448 self._ChBOX_relevant.SetValue(bool(rev[3]))
449 break
450
451 self._ChBOX_sign_all_pages.SetValue(self.__reviewing_doc)
452
453 return True
454
456 self._LCTRL_existing_reviews.DeleteAllItems()
457 revs = self.__part.get_reviews()
458 if len(revs) == 0:
459 return True
460
461 review_by_responsible_doc = None
462 reviews_by_others = []
463 for rev in revs:
464 if rev['is_review_by_responsible_reviewer'] and not rev['is_your_review']:
465 review_by_responsible_doc = rev
466 if not (rev['is_review_by_responsible_reviewer'] or rev['is_your_review']):
467 reviews_by_others.append(rev)
468
469 if review_by_responsible_doc is not None:
470 row_num = self._LCTRL_existing_reviews.InsertStringItem(sys.maxint, label=review_by_responsible_doc[0])
471 self._LCTRL_existing_reviews.SetItemTextColour(row_num, col=wx.BLUE)
472 self._LCTRL_existing_reviews.SetStringItem(index = row_num, col=0, label=review_by_responsible_doc[0])
473 self._LCTRL_existing_reviews.SetStringItem(index = row_num, col=1, label=review_by_responsible_doc[1].strftime('%x %H:%M'))
474 if review_by_responsible_doc['is_technically_abnormal']:
475 self._LCTRL_existing_reviews.SetStringItem(index = row_num, col=2, label=u'X')
476 if review_by_responsible_doc['clinically_relevant']:
477 self._LCTRL_existing_reviews.SetStringItem(index = row_num, col=3, label=u'X')
478 self._LCTRL_existing_reviews.SetStringItem(index = row_num, col=4, label=review_by_responsible_doc[6])
479 row_num += 1
480 for rev in reviews_by_others:
481 row_num = self._LCTRL_existing_reviews.InsertStringItem(sys.maxint, label=rev[0])
482 self._LCTRL_existing_reviews.SetStringItem(index = row_num, col=0, label=rev[0])
483 self._LCTRL_existing_reviews.SetStringItem(index = row_num, col=1, label=rev[1].strftime('%x %H:%M'))
484 if rev['is_technically_abnormal']:
485 self._LCTRL_existing_reviews.SetStringItem(index = row_num, col=2, label=u'X')
486 if rev['clinically_relevant']:
487 self._LCTRL_existing_reviews.SetStringItem(index = row_num, col=3, label=u'X')
488 self._LCTRL_existing_reviews.SetStringItem(index = row_num, col=4, label=rev[6])
489 return True
490
491
492
565
567 state = self._ChBOX_review.GetValue()
568 self._ChBOX_abnormal.Enable(enable = state)
569 self._ChBOX_relevant.Enable(enable = state)
570 self._ChBOX_responsible.Enable(enable = state)
571
573 """Per Jim: Changing the doc type happens a lot more often
574 then correcting spelling, hence select-all on getting focus.
575 """
576 self._PhWheel_doc_type.SetSelection(-1, -1)
577
579 pk_doc_type = self._PhWheel_doc_type.GetData()
580 if pk_doc_type is None:
581 self._PRW_doc_comment.unset_context(context = 'pk_doc_type')
582 else:
583 self._PRW_doc_comment.set_context(context = 'pk_doc_type', val = pk_doc_type)
584 return True
585
586 -class cScanIdxDocsPnl(wxgScanIdxPnl.wxgScanIdxPnl, gmPlugin.cPatientChange_PluginMixin):
607
608
609
612
614 pat = gmPerson.gmCurrentPatient()
615 if not pat.connected:
616 gmDispatcher.send(signal='statustext', msg=_('Cannot accept new documents. No active patient.'))
617 return
618
619
620 real_filenames = []
621 for pathname in filenames:
622 try:
623 files = os.listdir(pathname)
624 gmDispatcher.send(signal='statustext', msg=_('Extracting files from folder [%s] ...') % pathname)
625 for file in files:
626 fullname = os.path.join(pathname, file)
627 if not os.path.isfile(fullname):
628 continue
629 real_filenames.append(fullname)
630 except OSError:
631 real_filenames.append(pathname)
632
633 self.acquired_pages.extend(real_filenames)
634 self.__reload_LBOX_doc_pages()
635
638
639
640
644
645 - def _post_patient_selection(self, **kwds):
646 self.__init_ui_data()
647
648
649
651
652 self._PhWheel_episode.SetText('')
653 self._PhWheel_doc_type.SetText('')
654
655
656 fts = gmDateTime.cFuzzyTimestamp()
657 self._PhWheel_doc_date.SetText(fts.strftime('%Y-%m-%d'), fts)
658 self._PRW_doc_comment.SetText('')
659
660 self._PhWheel_reviewer.selection_only = True
661 me = gmPerson.gmCurrentProvider()
662 self._PhWheel_reviewer.SetText (
663 value = u'%s (%s%s %s)' % (me['short_alias'], me['title'], me['firstnames'], me['lastnames']),
664 data = me['pk_staff']
665 )
666
667
668 self._ChBOX_reviewed.SetValue(False)
669 self._ChBOX_abnormal.Disable()
670 self._ChBOX_abnormal.SetValue(False)
671 self._ChBOX_relevant.Disable()
672 self._ChBOX_relevant.SetValue(False)
673
674 self._TBOX_description.SetValue('')
675
676
677 self._LBOX_doc_pages.Clear()
678 self.acquired_pages = []
679
681 self._LBOX_doc_pages.Clear()
682 if len(self.acquired_pages) > 0:
683 for i in range(len(self.acquired_pages)):
684 fname = self.acquired_pages[i]
685 self._LBOX_doc_pages.Append(_('part %s: %s' % (i+1, fname)), fname)
686
688 title = _('saving document')
689
690 if self.acquired_pages is None or len(self.acquired_pages) == 0:
691 dbcfg = gmCfg.cCfgSQL()
692 allow_empty = bool(dbcfg.get2 (
693 option = u'horstspace.scan_index.allow_partless_documents',
694 workplace = gmSurgery.gmCurrentPractice().active_workplace,
695 bias = 'user',
696 default = False
697 ))
698 if allow_empty:
699 save_empty = gmGuiHelpers.gm_show_question (
700 aMessage = _('No parts to save. Really save an empty document as a reference ?'),
701 aTitle = title
702 )
703 if not save_empty:
704 return False
705 else:
706 gmGuiHelpers.gm_show_error (
707 aMessage = _('No parts to save. Aquire some parts first.'),
708 aTitle = title
709 )
710 return False
711
712 doc_type_pk = self._PhWheel_doc_type.GetData(can_create = True)
713 if doc_type_pk is None:
714 gmGuiHelpers.gm_show_error (
715 aMessage = _('No document type applied. Choose a document type'),
716 aTitle = title
717 )
718 return False
719
720
721
722
723
724
725
726
727
728 if self._PhWheel_episode.GetValue().strip() == '':
729 gmGuiHelpers.gm_show_error (
730 aMessage = _('You must select an episode to save this document under.'),
731 aTitle = title
732 )
733 return False
734
735 if self._PhWheel_reviewer.GetData() is None:
736 gmGuiHelpers.gm_show_error (
737 aMessage = _('You need to select from the list of staff members the doctor who is intended to sign the document.'),
738 aTitle = title
739 )
740 return False
741
742 return True
743
745
746 if not reconfigure:
747 dbcfg = gmCfg.cCfgSQL()
748 device = dbcfg.get2 (
749 option = 'external.xsane.default_device',
750 workplace = gmSurgery.gmCurrentPractice().active_workplace,
751 bias = 'workplace',
752 default = ''
753 )
754 if device.strip() == u'':
755 device = None
756 if device is not None:
757 return device
758
759 try:
760 devices = self.scan_module.get_devices()
761 except:
762 _log.exception('cannot retrieve list of image sources')
763 gmDispatcher.send(signal = 'statustext', msg = _('There is no scanner support installed on this machine.'))
764 return None
765
766 if devices is None:
767
768
769 return None
770
771 if len(devices) == 0:
772 gmDispatcher.send(signal = 'statustext', msg = _('Cannot find an active scanner.'))
773 return None
774
775
776
777
778
779 device = gmListWidgets.get_choices_from_list (
780 parent = self,
781 msg = _('Select an image capture device'),
782 caption = _('device selection'),
783 choices = [ '%s (%s)' % (d[2], d[0]) for d in devices ],
784 columns = [_('Device')],
785 data = devices,
786 single_selection = True
787 )
788 if device is None:
789 return None
790
791
792 return device[0]
793
794
795
797
798 chosen_device = self.get_device_to_use()
799
800 tmpdir = os.path.expanduser(os.path.join('~', '.gnumed', 'tmp'))
801 try:
802 gmTools.mkdir(tmpdir)
803 except:
804 tmpdir = None
805
806
807
808 try:
809 fnames = self.scan_module.acquire_pages_into_files (
810 device = chosen_device,
811 delay = 5,
812 tmpdir = tmpdir,
813 calling_window = self
814 )
815 except ImportError:
816 gmGuiHelpers.gm_show_error (
817 aMessage = _(
818 'No pages could be acquired from the source.\n\n'
819 'This may mean the scanner driver is not properly installed\n\n'
820 'On Windows you must install the TWAIN Python module\n'
821 'while on Linux and MacOSX it is recommended to install\n'
822 'the XSane package.'
823 ),
824 aTitle = _('acquiring page')
825 )
826 return None
827
828 if len(fnames) == 0:
829 return True
830
831 self.acquired_pages.extend(fnames)
832 self.__reload_LBOX_doc_pages()
833
834 return True
835
837
838 dlg = wx.FileDialog (
839 parent = None,
840 message = _('Choose a file'),
841 defaultDir = os.path.expanduser(os.path.join('~', 'gnumed')),
842 defaultFile = '',
843 wildcard = "%s (*)|*|TIFFs (*.tif)|*.tif|JPEGs (*.jpg)|*.jpg|%s (*.*)|*.*" % (_('all files'), _('all files (Win)')),
844 style = wx.OPEN | wx.HIDE_READONLY | wx.FILE_MUST_EXIST | wx.MULTIPLE
845 )
846 result = dlg.ShowModal()
847 if result != wx.ID_CANCEL:
848 files = dlg.GetPaths()
849 for file in files:
850 self.acquired_pages.append(file)
851 self.__reload_LBOX_doc_pages()
852 dlg.Destroy()
853
855
856 page_idx = self._LBOX_doc_pages.GetSelection()
857 if page_idx == -1:
858 gmGuiHelpers.gm_show_info (
859 aMessage = _('You must select a part before you can view it.'),
860 aTitle = _('displaying part')
861 )
862 return None
863
864 page_fname = self._LBOX_doc_pages.GetClientData(page_idx)
865 (result, msg) = gmMimeLib.call_viewer_on_file(page_fname)
866 if not result:
867 gmGuiHelpers.gm_show_warning (
868 aMessage = _('Cannot display document part:\n%s') % msg,
869 aTitle = _('displaying part')
870 )
871 return None
872 return 1
873
875 page_idx = self._LBOX_doc_pages.GetSelection()
876 if page_idx == -1:
877 gmGuiHelpers.gm_show_info (
878 aMessage = _('You must select a part before you can delete it.'),
879 aTitle = _('deleting part')
880 )
881 return None
882 page_fname = self._LBOX_doc_pages.GetClientData(page_idx)
883
884
885 self.acquired_pages[page_idx:(page_idx+1)] = []
886
887
888 self.__reload_LBOX_doc_pages()
889
890
891 do_delete = gmGuiHelpers.gm_show_question (
892 _(
893 """Do you want to permanently delete the file
894
895 [%s]
896
897 from your computer ?
898
899 If it is a temporary file for a page you just scanned
900 in this makes a lot of sense. In other cases you may
901 not want to lose the file.
902
903 Pressing [YES] will permanently remove the file
904 from your computer.""") % page_fname,
905 _('deleting part')
906 )
907 if do_delete:
908 try:
909 os.remove(page_fname)
910 except:
911 _log.exception('Error deleting file.')
912 gmGuiHelpers.gm_show_error (
913 aMessage = _('Cannot delete part in file [%s].\n\nYou may not have write access to it.') % page_fname,
914 aTitle = _('deleting part')
915 )
916
917 return 1
918
920
921 if not self.__valid_for_save():
922 return False
923
924 wx.BeginBusyCursor()
925
926 pat = gmPerson.gmCurrentPatient()
927 doc_folder = pat.get_document_folder()
928 emr = pat.get_emr()
929
930
931 pk_episode = self._PhWheel_episode.GetData()
932 if pk_episode is None:
933 episode = emr.add_episode (
934 episode_name = self._PhWheel_episode.GetValue().strip(),
935 is_open = True
936 )
937 if episode is None:
938 wx.EndBusyCursor()
939 gmGuiHelpers.gm_show_error (
940 aMessage = _('Cannot start episode [%s].') % self._PhWheel_episode.GetValue().strip(),
941 aTitle = _('saving document')
942 )
943 return False
944 pk_episode = episode['pk_episode']
945
946 encounter = emr.active_encounter['pk_encounter']
947 document_type = self._PhWheel_doc_type.GetData()
948 new_doc = doc_folder.add_document(document_type, encounter, pk_episode)
949 if new_doc is None:
950 wx.EndBusyCursor()
951 gmGuiHelpers.gm_show_error (
952 aMessage = _('Cannot create new document.'),
953 aTitle = _('saving document')
954 )
955 return False
956
957
958
959 new_doc['clin_when'] = self._PhWheel_doc_date.GetData().get_pydt()
960
961 ref = gmMedDoc.get_ext_ref()
962 if ref is not None:
963 new_doc['ext_ref'] = ref
964
965 comment = self._PRW_doc_comment.GetLineText(0).strip()
966 if comment != u'':
967 new_doc['comment'] = comment
968
969 if not new_doc.save_payload():
970 wx.EndBusyCursor()
971 gmGuiHelpers.gm_show_error (
972 aMessage = _('Cannot update document metadata.'),
973 aTitle = _('saving document')
974 )
975 return False
976
977 description = self._TBOX_description.GetValue().strip()
978 if description != '':
979 if not new_doc.add_description(description):
980 wx.EndBusyCursor()
981 gmGuiHelpers.gm_show_error (
982 aMessage = _('Cannot add document description.'),
983 aTitle = _('saving document')
984 )
985 return False
986
987
988 success, msg, filename = new_doc.add_parts_from_files(files=self.acquired_pages, reviewer=self._PhWheel_reviewer.GetData())
989 if not success:
990 wx.EndBusyCursor()
991 gmGuiHelpers.gm_show_error (
992 aMessage = msg,
993 aTitle = _('saving document')
994 )
995 return False
996
997
998 if self._ChBOX_reviewed.GetValue():
999 if not new_doc.set_reviewed (
1000 technically_abnormal = self._ChBOX_abnormal.GetValue(),
1001 clinically_relevant = self._ChBOX_relevant.GetValue()
1002 ):
1003 msg = _('Error setting "reviewed" status of new document.')
1004
1005 gmHooks.run_hook_script(hook = u'after_new_doc_created')
1006
1007
1008 cfg = gmCfg.cCfgSQL()
1009 show_id = bool (
1010 cfg.get2 (
1011 option = 'horstspace.scan_index.show_doc_id',
1012 workplace = gmSurgery.gmCurrentPractice().active_workplace,
1013 bias = 'user'
1014 )
1015 )
1016 wx.EndBusyCursor()
1017 if show_id and (ref is not None):
1018 msg = _(
1019 """The reference ID for the new document is:
1020
1021 <%s>
1022
1023 You probably want to write it down on the
1024 original documents.
1025
1026 If you don't care about the ID you can switch
1027 off this message in the GNUmed configuration.""") % ref
1028 gmGuiHelpers.gm_show_info (
1029 aMessage = msg,
1030 aTitle = _('saving document')
1031 )
1032 else:
1033 gmDispatcher.send(signal='statustext', msg=_('Successfully saved new document.'))
1034
1035 self.__init_ui_data()
1036 return True
1037
1039 self.__init_ui_data()
1040
1042 self._ChBOX_abnormal.Enable(enable = self._ChBOX_reviewed.GetValue())
1043 self._ChBOX_relevant.Enable(enable = self._ChBOX_reviewed.GetValue())
1044
1046 pk_doc_type = self._PhWheel_doc_type.GetData()
1047 if pk_doc_type is None:
1048 self._PRW_doc_comment.unset_context(context = 'pk_doc_type')
1049 else:
1050 self._PRW_doc_comment.set_context(context = 'pk_doc_type', val = pk_doc_type)
1051 return True
1052
1054 """A panel with a document tree which can be sorted."""
1055
1056
1057
1062
1067
1072
1077
1078 -class cDocTree(wx.TreeCtrl, gmRegetMixin.cRegetOnPaintMixin):
1079
1080 """This wx.TreeCtrl derivative displays a tree view of stored medical documents.
1081
1082 It listens to document and patient changes and updated itself accordingly.
1083 """
1084 _sort_modes = ['age', 'review', 'episode', 'type']
1085 _root_node_labels = None
1086
1087 - def __init__(self, parent, id, *args, **kwds):
1088 """Set up our specialised tree.
1089 """
1090 kwds['style'] = wx.TR_NO_BUTTONS | wx.NO_BORDER
1091 wx.TreeCtrl.__init__(self, parent, id, *args, **kwds)
1092
1093 gmRegetMixin.cRegetOnPaintMixin.__init__(self)
1094
1095 tmp = _('available documents (%s)')
1096 unsigned = _('unsigned (%s) on top') % u'\u270D'
1097 cDocTree._root_node_labels = {
1098 'age': tmp % _('most recent on top'),
1099 'review': tmp % unsigned,
1100 'episode': tmp % _('sorted by episode'),
1101 'type': tmp % _('sorted by type')
1102 }
1103
1104 self.root = None
1105 self.__sort_mode = 'age'
1106
1107 self.__build_context_menus()
1108 self.__register_interests()
1109 self._schedule_data_reget()
1110
1111
1112
1114
1115 node = self.GetSelection()
1116 node_data = self.GetPyData(node)
1117
1118 if not isinstance(node_data, gmMedDoc.cMedDocPart):
1119 return True
1120
1121 self.__display_part(part = node_data)
1122 return True
1123
1124
1125
1127 return self.__sort_mode
1128
1146
1147 sort_mode = property(_get_sort_mode, _set_sort_mode)
1148
1149
1150
1152 curr_pat = gmPerson.gmCurrentPatient()
1153 if not curr_pat.connected:
1154 gmDispatcher.send(signal = 'statustext', msg = _('Cannot load documents. No active patient.'))
1155 return False
1156
1157 if not self.__populate_tree():
1158 return False
1159
1160 return True
1161
1162
1163
1165
1166 wx.EVT_TREE_ITEM_ACTIVATED (self, self.GetId(), self._on_activate)
1167 wx.EVT_TREE_ITEM_RIGHT_CLICK (self, self.GetId(), self.__on_right_click)
1168
1169
1170
1171 gmDispatcher.connect(signal = u'pre_patient_selection', receiver = self._on_pre_patient_selection)
1172 gmDispatcher.connect(signal = u'post_patient_selection', receiver = self._on_post_patient_selection)
1173 gmDispatcher.connect(signal = u'doc_mod_db', receiver = self._on_doc_mod_db)
1174 gmDispatcher.connect(signal = u'doc_page_mod_db', receiver = self._on_doc_page_mod_db)
1175
1177
1178
1179 self.__part_context_menu = wx.Menu(title = _('part menu'))
1180
1181 ID = wx.NewId()
1182 self.__part_context_menu.Append(ID, _('Display part'))
1183 wx.EVT_MENU(self.__part_context_menu, ID, self.__display_curr_part)
1184
1185 ID = wx.NewId()
1186 self.__part_context_menu.Append(ID, _('%s Sign/Edit properties') % u'\u270D')
1187 wx.EVT_MENU(self.__part_context_menu, ID, self.__review_curr_part)
1188
1189 self.__part_context_menu.AppendSeparator()
1190
1191 ID = wx.NewId()
1192 self.__part_context_menu.Append(ID, _('Print part'))
1193 wx.EVT_MENU(self.__part_context_menu, ID, self.__print_part)
1194
1195 ID = wx.NewId()
1196 self.__part_context_menu.Append(ID, _('Fax part'))
1197 wx.EVT_MENU(self.__part_context_menu, ID, self.__fax_part)
1198
1199 ID = wx.NewId()
1200 self.__part_context_menu.Append(ID, _('Mail part'))
1201 wx.EVT_MENU(self.__part_context_menu, ID, self.__mail_part)
1202
1203 self.__part_context_menu.AppendSeparator()
1204
1205
1206 self.__doc_context_menu = wx.Menu(title = _('document menu'))
1207
1208 ID = wx.NewId()
1209 self.__doc_context_menu.Append(ID, _('%s Sign/Edit properties') % u'\u270D')
1210 wx.EVT_MENU(self.__doc_context_menu, ID, self.__review_curr_part)
1211
1212 self.__doc_context_menu.AppendSeparator()
1213
1214 ID = wx.NewId()
1215 self.__doc_context_menu.Append(ID, _('Print all parts'))
1216 wx.EVT_MENU(self.__doc_context_menu, ID, self.__print_doc)
1217
1218 ID = wx.NewId()
1219 self.__doc_context_menu.Append(ID, _('Fax all parts'))
1220 wx.EVT_MENU(self.__doc_context_menu, ID, self.__fax_doc)
1221
1222 ID = wx.NewId()
1223 self.__doc_context_menu.Append(ID, _('Mail all parts'))
1224 wx.EVT_MENU(self.__doc_context_menu, ID, self.__mail_doc)
1225
1226 ID = wx.NewId()
1227 self.__doc_context_menu.Append(ID, _('Export all parts'))
1228 wx.EVT_MENU(self.__doc_context_menu, ID, self.__export_doc_to_disk)
1229
1230 self.__doc_context_menu.AppendSeparator()
1231
1232 ID = wx.NewId()
1233 self.__doc_context_menu.Append(ID, _('Delete document'))
1234 wx.EVT_MENU(self.__doc_context_menu, ID, self.__delete_document)
1235
1236 ID = wx.NewId()
1237 self.__doc_context_menu.Append(ID, _('Access external original'))
1238 wx.EVT_MENU(self.__doc_context_menu, ID, self.__access_external_original)
1239
1240 ID = wx.NewId()
1241 self.__doc_context_menu.Append(ID, _('Edit corresponding encounter'))
1242 wx.EVT_MENU(self.__doc_context_menu, ID, self.__edit_encounter_details)
1243
1244
1245
1246
1247 ID = wx.NewId()
1248 self.__doc_context_menu.Append(ID, _('Manage descriptions'))
1249 wx.EVT_MENU(self.__doc_context_menu, ID, self.__manage_document_descriptions)
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1267
1268 wx.BeginBusyCursor()
1269
1270
1271 if self.root is not None:
1272 self.DeleteAllItems()
1273
1274
1275 self.root = self.AddRoot(cDocTree._root_node_labels[self.__sort_mode], -1, -1)
1276 self.SetPyData(self.root, None)
1277 self.SetItemHasChildren(self.root, False)
1278
1279
1280 curr_pat = gmPerson.gmCurrentPatient()
1281 docs_folder = curr_pat.get_document_folder()
1282 docs = docs_folder.get_documents()
1283 if docs is None:
1284 gmGuiHelpers.gm_show_error (
1285 aMessage = _('Error searching documents.'),
1286 aTitle = _('loading document list')
1287 )
1288
1289 wx.EndBusyCursor()
1290 return True
1291
1292 if len(docs) == 0:
1293 wx.EndBusyCursor()
1294 return True
1295
1296
1297 self.SetItemHasChildren(self.root, True)
1298
1299
1300 intermediate_nodes = {}
1301 for doc in docs:
1302
1303 parts = doc.get_parts()
1304
1305 cmt = gmTools.coalesce(doc['comment'], _('no comment available'))
1306 page_num = len(parts)
1307 ref = gmTools.coalesce(initial = doc['ext_ref'], instead = u'', template_initial = u', \u00BB%s\u00AB')
1308
1309 if doc.has_unreviewed_parts():
1310 review = gmTools.u_writing_hand
1311 else:
1312 review = u''
1313
1314 label = _('%s%7s %s: %s (%s part(s)%s)') % (
1315 review,
1316 doc['clin_when'].strftime('%m/%Y'),
1317 doc['l10n_type'][:26],
1318 cmt,
1319 page_num,
1320 ref
1321 )
1322
1323
1324 if self.__sort_mode == 'episode':
1325 if not intermediate_nodes.has_key(doc['episode']):
1326 intermediate_nodes[doc['episode']] = self.AppendItem(parent = self.root, text = doc['episode'])
1327 self.SetItemBold(intermediate_nodes[doc['episode']], bold = True)
1328 self.SetPyData(intermediate_nodes[doc['episode']], None)
1329 parent = intermediate_nodes[doc['episode']]
1330 elif self.__sort_mode == 'type':
1331 if not intermediate_nodes.has_key(doc['l10n_type']):
1332 intermediate_nodes[doc['l10n_type']] = self.AppendItem(parent = self.root, text = doc['l10n_type'])
1333 self.SetItemBold(intermediate_nodes[doc['l10n_type']], bold = True)
1334 self.SetPyData(intermediate_nodes[doc['l10n_type']], None)
1335 parent = intermediate_nodes[doc['l10n_type']]
1336 else:
1337 parent = self.root
1338
1339 doc_node = self.AppendItem(parent = parent, text = label)
1340
1341 self.SetPyData(doc_node, doc)
1342 if len(parts) > 0:
1343 self.SetItemHasChildren(doc_node, True)
1344
1345
1346 for part in parts:
1347
1348 pg = _('part %2s') % part['seq_idx']
1349 cmt = gmTools.coalesce(part['obj_comment'], u'', u': %s%%s%s' % (gmTools.u_left_double_angle_quote, gmTools.u_right_double_angle_quote))
1350 sz = gmTools.size2str(part['size'])
1351 rev = gmTools.bool2str (
1352 boolean = part['reviewed'] or part['reviewed_by_you'] or part['reviewed_by_intended_reviewer'],
1353 true_str = u'',
1354 false_str = gmTools.u_writing_hand
1355 )
1356
1357
1358
1359
1360
1361
1362 label = '%s%s (%s)%s' % (rev, pg, sz, cmt)
1363
1364 part_node = self.AppendItem(parent = doc_node, text = label)
1365 self.SetPyData(part_node, part)
1366
1367 self.__sort_nodes()
1368 self.SelectItem(self.root)
1369
1370
1371
1372 self.Expand(self.root)
1373 if self.__sort_mode in ['episode', 'type']:
1374 for key in intermediate_nodes.keys():
1375 self.Expand(intermediate_nodes[key])
1376
1377 wx.EndBusyCursor()
1378 return True
1379
1381 """Used in sorting items.
1382
1383 -1: 1 < 2
1384 0: 1 = 2
1385 1: 1 > 2
1386 """
1387 item1 = self.GetPyData(node1)
1388 item2 = self.GetPyData(node2)
1389
1390
1391 if isinstance(item1, gmMedDoc.cMedDoc):
1392
1393 date_field = 'clin_when'
1394
1395
1396 if self.__sort_mode == 'age':
1397
1398 if item1[date_field] > item2[date_field]:
1399 return -1
1400 if item1[date_field] == item2[date_field]:
1401 return 0
1402 return 1
1403
1404 elif self.__sort_mode == 'episode':
1405 if item1['episode'] < item2['episode']:
1406 return -1
1407 if item1['episode'] == item2['episode']:
1408
1409 if item1[date_field] > item2[date_field]:
1410 return -1
1411 if item1[date_field] == item2[date_field]:
1412 return 0
1413 return 1
1414 return 1
1415
1416 elif self.__sort_mode == 'review':
1417
1418 if item1.has_unreviewed_parts() == item2.has_unreviewed_parts():
1419
1420 if item1[date_field] > item2[date_field]:
1421 return -1
1422 if item1[date_field] == item2[date_field]:
1423 return 0
1424 return 1
1425 if item1.has_unreviewed_parts():
1426 return -1
1427 return 1
1428
1429 elif self.__sort_mode == 'type':
1430 if item1['l10n_type'] < item2['l10n_type']:
1431 return -1
1432 if item1['l10n_type'] == item2['l10n_type']:
1433
1434 if item1[date_field] > item2[date_field]:
1435 return -1
1436 if item1[date_field] == item2[date_field]:
1437 return 0
1438 return 1
1439 return 1
1440
1441 else:
1442 _log.error('unknown document sort mode [%s], reverse-sorting by age', self.__sort_mode)
1443
1444 if item1[date_field] > item2[date_field]:
1445 return -1
1446 if item1[date_field] == item2[date_field]:
1447 return 0
1448 return 1
1449
1450
1451 if isinstance(item1, gmMedDoc.cMedDocPart):
1452
1453
1454 if item1['seq_idx'] < item2['seq_idx']:
1455 return -1
1456 if item1['seq_idx'] == item2['seq_idx']:
1457 return 0
1458 return 1
1459
1460
1461 if None in [item1, item2]:
1462 if node1 < node2:
1463 return -1
1464 if node1 == node2:
1465 return 0
1466 else:
1467 if item1 < item2:
1468 return -1
1469 if item1 == item2:
1470 return 0
1471 return 1
1472
1473
1474
1476
1477 wx.CallAfter(self._schedule_data_reget)
1478
1479 - def _on_doc_page_mod_db(self, *args, **kwargs):
1480
1481 wx.CallAfter(self._schedule_data_reget)
1482
1484
1485
1486
1487 if self.root is not None:
1488 self.DeleteAllItems()
1489 self.root = None
1490
1491 - def _on_post_patient_selection(self, *args, **kwargs):
1492
1493 self._schedule_data_reget()
1494
1496 node = event.GetItem()
1497 node_data = self.GetPyData(node)
1498
1499
1500 if node_data is None:
1501 return None
1502
1503
1504 if isinstance(node_data, gmMedDoc.cMedDoc):
1505 self.Toggle(node)
1506 return True
1507
1508
1509 if type(node_data) == type('string'):
1510 self.Toggle(node)
1511 return True
1512
1513 self.__display_part(part = node_data)
1514 return True
1515
1517
1518 node = evt.GetItem()
1519 self.__curr_node_data = self.GetPyData(node)
1520
1521
1522 if self.__curr_node_data is None:
1523 return None
1524
1525
1526 if isinstance(self.__curr_node_data, gmMedDoc.cMedDoc):
1527 self.__handle_doc_context()
1528
1529
1530 if isinstance(self.__curr_node_data, gmMedDoc.cMedDocPart):
1531 self.__handle_part_context()
1532
1533 del self.__curr_node_data
1534 evt.Skip()
1535
1538
1540 self.__display_part(part=self.__curr_node_data)
1541
1543 self.__review_part(part = self.__curr_node_data)
1544
1547
1548
1549
1551 if start_node is None:
1552 start_node = self.root
1553
1554
1555
1556 if not start_node.IsOk():
1557 return True
1558
1559 self.SortChildren(start_node)
1560
1561 child_node, cookie = self.GetFirstChild(start_node)
1562 while child_node.IsOk():
1563 self.__sort_nodes(start_node = child_node)
1564 child_node, cookie = self.GetNextChild(start_node, cookie)
1565
1566 return
1567
1569 self.PopupMenu(self.__doc_context_menu, wx.DefaultPosition)
1570
1572
1573
1574 if self.__curr_node_data['type'] == 'patient photograph':
1575 ID = wx.NewId()
1576 self.__part_context_menu.Append(ID, _('Activate as current photo'))
1577 wx.EVT_MENU(self.__part_context_menu, ID, self.__activate_as_current_photo)
1578 else:
1579 ID = None
1580
1581 self.PopupMenu(self.__part_context_menu, wx.DefaultPosition)
1582
1583 if ID is not None:
1584 self.__part_context_menu.Delete(ID)
1585
1586
1587
1589 """Display document part."""
1590
1591
1592 if part['size'] == 0:
1593 _log.debug('cannot display part [%s] - 0 bytes', part['pk_obj'])
1594 gmGuiHelpers.gm_show_error (
1595 aMessage = _('Document part does not seem to exist in database !'),
1596 aTitle = _('showing document')
1597 )
1598 return None
1599
1600 wx.BeginBusyCursor()
1601
1602 cfg = gmCfg.cCfgSQL()
1603
1604
1605 tmp_dir = gmTools.coalesce (
1606 cfg.get2 (
1607 option = "horstspace.tmp_dir",
1608 workplace = gmSurgery.gmCurrentPractice().active_workplace,
1609 bias = 'workplace'
1610 ),
1611 os.path.expanduser(os.path.join('~', '.gnumed', 'tmp'))
1612 )
1613 _log.debug("temporary directory [%s]", tmp_dir)
1614
1615
1616 chunksize = int(
1617 cfg.get2 (
1618 option = "horstspace.blob_export_chunk_size",
1619 workplace = gmSurgery.gmCurrentPractice().active_workplace,
1620 bias = 'workplace',
1621 default = default_chunksize
1622 ))
1623
1624
1625 block_during_view = bool( cfg.get2 (
1626 option = 'horstspace.document_viewer.block_during_view',
1627 workplace = gmSurgery.gmCurrentPractice().active_workplace,
1628 bias = 'user',
1629 default = None
1630 ))
1631
1632
1633 successful, msg = part.display_via_mime (
1634 tmpdir = tmp_dir,
1635 chunksize = chunksize,
1636 block = block_during_view
1637 )
1638
1639 wx.EndBusyCursor()
1640
1641 if not successful:
1642 gmGuiHelpers.gm_show_error (
1643 aMessage = _('Cannot display document part:\n%s') % msg,
1644 aTitle = _('showing document')
1645 )
1646 return None
1647
1648
1649
1650
1651
1652 review_after_display = int(cfg.get2 (
1653 option = 'horstspace.document_viewer.review_after_display',
1654 workplace = gmSurgery.gmCurrentPractice().active_workplace,
1655 bias = 'user',
1656 default = 2
1657 ))
1658 if review_after_display == 1:
1659 self.__review_part(part=part)
1660 elif review_after_display == 2:
1661 review_by_me = filter(lambda rev: rev['is_your_review'], part.get_reviews())
1662 if len(review_by_me) == 0:
1663 self.__review_part(part=part)
1664
1665 return True
1666
1668 dlg = cReviewDocPartDlg (
1669 parent = self,
1670 id = -1,
1671 part = part
1672 )
1673 dlg.ShowModal()
1674 dlg.Destroy()
1675
1677
1678 gmHooks.run_hook_script(hook = u'before_%s_doc_part' % action)
1679
1680 wx.BeginBusyCursor()
1681
1682
1683 found, external_cmd = gmShellAPI.detect_external_binary(u'gm-%s_doc' % action)
1684 if not found:
1685 found, external_cmd = gmShellAPI.detect_external_binary(u'gm-%s_doc.bat' % action)
1686 if not found:
1687 _log.error('neither of gm-%s_doc or gm-%s_doc.bat found', action, action)
1688 wx.EndBusyCursor()
1689 gmGuiHelpers.gm_show_error (
1690 _('Cannot %(l10n_action)s document part - %(l10n_action)s command not found.\n'
1691 '\n'
1692 'Either of gm_%(action)s_doc.sh or gm_%(action)s_doc.bat\n'
1693 'must be in the execution path. The command will\n'
1694 'be passed the filename to %(l10n_action)s.'
1695 ) % {'action': action, 'l10n_action': l10n_action},
1696 _('Processing document part: %s') % l10n_action
1697 )
1698 return
1699
1700 cfg = gmCfg.cCfgSQL()
1701
1702
1703 tmp_dir = gmTools.coalesce (
1704 cfg.get2 (
1705 option = "horstspace.tmp_dir",
1706 workplace = gmSurgery.gmCurrentPractice().active_workplace,
1707 bias = 'workplace'
1708 ),
1709 os.path.expanduser(os.path.join('~', '.gnumed', 'tmp'))
1710 )
1711 _log.debug("temporary directory [%s]", tmp_dir)
1712
1713
1714 chunksize = int(cfg.get2 (
1715 option = "horstspace.blob_export_chunk_size",
1716 workplace = gmSurgery.gmCurrentPractice().active_workplace,
1717 bias = 'workplace',
1718 default = default_chunksize
1719 ))
1720
1721 part_file = self.__curr_node_data.export_to_file (
1722 aTempDir = tmp_dir,
1723 aChunkSize = chunksize
1724 )
1725
1726 cmd = u'%s %s' % (external_cmd, part_file)
1727 success = gmShellAPI.run_command_in_shell (
1728 command = cmd,
1729 blocking = False
1730 )
1731
1732 wx.EndBusyCursor()
1733
1734 if not success:
1735 _log.error('%s command failed: [%s]', action, cmd)
1736 gmGuiHelpers.gm_show_error (
1737 _('Cannot %(l10n_action)s document part - %(l10n_action)s command failed.\n'
1738 '\n'
1739 'You may need to check and fix either of\n'
1740 ' gm_%(action)s_doc.sh (Unix/Mac) or\n'
1741 ' gm_%(action)s_doc.bat (Windows)\n'
1742 '\n'
1743 'The command is passed the filename to %(l10n_action)s.'
1744 ) % {'action': action, 'l10n_action': l10n_action},
1745 _('Processing document part: %s') % l10n_action
1746 )
1747
1748
1750 self.__process_part(action = u'print', l10n_action = _('print'))
1751
1753 self.__process_part(action = u'fax', l10n_action = _('fax'))
1754
1756 self.__process_part(action = u'mail', l10n_action = _('mail'))
1757
1758
1759
1764
1766
1767 gmHooks.run_hook_script(hook = u'before_%s_doc' % action)
1768
1769 wx.BeginBusyCursor()
1770
1771
1772 found, external_cmd = gmShellAPI.detect_external_binary(u'gm-%s_doc' % action)
1773 if not found:
1774 found, external_cmd = gmShellAPI.detect_external_binary(u'gm-%s_doc.bat' % action)
1775 if not found:
1776 _log.error('neither of gm-%s_doc or gm-%s_doc.bat found', action, action)
1777 wx.EndBusyCursor()
1778 gmGuiHelpers.gm_show_error (
1779 _('Cannot %(l10n_action)s document - %(l10n_action)s command not found.\n'
1780 '\n'
1781 'Either of gm_%(action)s_doc.sh or gm_%(action)s_doc.bat\n'
1782 'must be in the execution path. The command will\n'
1783 'be passed a list of filenames to %(l10n_action)s.'
1784 ) % {'action': action, 'l10n_action': l10n_action},
1785 _('Processing document: %s') % l10n_action
1786 )
1787 return
1788
1789 cfg = gmCfg.cCfgSQL()
1790
1791
1792 tmp_dir = gmTools.coalesce (
1793 cfg.get2 (
1794 option = "horstspace.tmp_dir",
1795 workplace = gmSurgery.gmCurrentPractice().active_workplace,
1796 bias = 'workplace'
1797 ),
1798 os.path.expanduser(os.path.join('~', '.gnumed', 'tmp'))
1799 )
1800 _log.debug("temporary directory [%s]", tmp_dir)
1801
1802
1803 chunksize = int(cfg.get2 (
1804 option = "horstspace.blob_export_chunk_size",
1805 workplace = gmSurgery.gmCurrentPractice().active_workplace,
1806 bias = 'workplace',
1807 default = default_chunksize
1808 ))
1809
1810 part_files = self.__curr_node_data.export_parts_to_files (
1811 export_dir = tmp_dir,
1812 chunksize = chunksize
1813 )
1814
1815 cmd = external_cmd + u' ' + u' '.join(part_files)
1816 success = gmShellAPI.run_command_in_shell (
1817 command = cmd,
1818 blocking = False
1819 )
1820
1821 wx.EndBusyCursor()
1822
1823 if not success:
1824 _log.error('%s command failed: [%s]', action, cmd)
1825 gmGuiHelpers.gm_show_error (
1826 _('Cannot %(l10n_action)s document - %(l10n_action)s command failed.\n'
1827 '\n'
1828 'You may need to check and fix either of\n'
1829 ' gm_%(action)s_doc.sh (Unix/Mac) or\n'
1830 ' gm_%(action)s_doc.bat (Windows)\n'
1831 '\n'
1832 'The command is passed a list of filenames to %(l10n_action)s.'
1833 ) % {'action': action, 'l10n_action': l10n_action},
1834 _('Processing document: %s') % l10n_action
1835 )
1836
1837
1839 self.__process_doc(action = u'print', l10n_action = _('print'))
1840
1842 self.__process_doc(action = u'fax', l10n_action = _('fax'))
1843
1845 self.__process_doc(action = u'mail', l10n_action = _('mail'))
1846
1848
1849 gmHooks.run_hook_script(hook = u'before_external_doc_access')
1850
1851 wx.BeginBusyCursor()
1852
1853
1854 found, external_cmd = gmShellAPI.detect_external_binary(u'gm_access_external_doc.sh')
1855 if not found:
1856 found, external_cmd = gmShellAPI.detect_external_binary(u'gm_access_external_doc.bat')
1857 if not found:
1858 _log.error('neither of gm_access_external_doc.sh or .bat found')
1859 wx.EndBusyCursor()
1860 gmGuiHelpers.gm_show_error (
1861 _('Cannot access external document - access command not found.\n'
1862 '\n'
1863 'Either of gm_access_external_doc.sh or *.bat must be\n'
1864 'in the execution path. The command will be passed the\n'
1865 'document type and the reference URL for processing.'
1866 ),
1867 _('Accessing external document')
1868 )
1869 return
1870
1871 cmd = u'%s "%s" "%s"' % (external_cmd, self.__curr_node_data['type'], self.__curr_node_data['ext_ref'])
1872 success = gmShellAPI.run_command_in_shell (
1873 command = cmd,
1874 blocking = False
1875 )
1876
1877 wx.EndBusyCursor()
1878
1879 if not success:
1880 _log.error('External access command failed: [%s]', cmd)
1881 gmGuiHelpers.gm_show_error (
1882 _('Cannot access external document - access command failed.\n'
1883 '\n'
1884 'You may need to check and fix either of\n'
1885 ' gm_access_external_doc.sh (Unix/Mac) or\n'
1886 ' gm_access_external_doc.bat (Windows)\n'
1887 '\n'
1888 'The command is passed the document type and the\n'
1889 'external reference URL on the command line.'
1890 ),
1891 _('Accessing external document')
1892 )
1893
1895 """Export document into directory.
1896
1897 - one file per object
1898 - into subdirectory named after patient
1899 """
1900 pat = gmPerson.gmCurrentPatient()
1901 dname = '%s-%s%s' % (
1902 self.__curr_node_data['l10n_type'],
1903 self.__curr_node_data['clin_when'].strftime('%Y-%m-%d'),
1904 gmTools.coalesce(self.__curr_node_data['ext_ref'], '', '-%s').replace(' ', '_')
1905 )
1906 def_dir = os.path.expanduser(os.path.join('~', 'gnumed', 'export', 'docs', pat['dirname'], dname))
1907 gmTools.mkdir(def_dir)
1908
1909 dlg = wx.DirDialog (
1910 parent = self,
1911 message = _('Save document into directory ...'),
1912 defaultPath = def_dir,
1913 style = wx.DD_DEFAULT_STYLE
1914 )
1915 result = dlg.ShowModal()
1916 dirname = dlg.GetPath()
1917 dlg.Destroy()
1918
1919 if result != wx.ID_OK:
1920 return True
1921
1922 wx.BeginBusyCursor()
1923
1924 cfg = gmCfg.cCfgSQL()
1925
1926
1927 chunksize = int(cfg.get2 (
1928 option = "horstspace.blob_export_chunk_size",
1929 workplace = gmSurgery.gmCurrentPractice().active_workplace,
1930 bias = 'workplace',
1931 default = default_chunksize
1932 ))
1933
1934 fnames = self.__curr_node_data.export_parts_to_files(export_dir = dirname, chunksize = chunksize)
1935
1936 wx.EndBusyCursor()
1937
1938 gmDispatcher.send(signal='statustext', msg=_('Successfully exported %s parts into the directory [%s].') % (len(fnames), dirname))
1939
1940 return True
1941
1952
1953
1954
1955 if __name__ == '__main__':
1956
1957 gmI18N.activate_locale()
1958 gmI18N.install_domain(domain = 'gnumed')
1959
1960
1961
1962 if (len(sys.argv) > 1) and (sys.argv[1] == 'test'):
1963
1964 pass
1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
1983
1984
1985
1986
1987
1988
1989
1990
1991
1992
1993
1994
1995
1996
1997
1998
1999
2000
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
2027
2028
2029
2030
2031
2032
2033
2034
2035
2036
2037
2038
2039
2040
2041
2042
2043
2044
2045
2046
2047
2048
2049
2050
2051
2052
2053
2054
2055
2056
2057
2058
2059
2060
2061
2062
2063
2064
2065
2066
2067
2068
2069
2070
2071
2072
2073
2074
2075
2076
2077
2078
2079
2080
2081
2082
2083
2084
2085
2086
2087
2088
2089
2090
2091
2092
2093
2094
2095
2096
2097
2098
2099
2100
2101
2102
2103
2104
2105
2106
2107
2108
2109
2110
2111
2112
2113
2114
2115
2116
2117
2118
2119
2120
2121
2122
2123
2124
2125
2126
2127
2128
2129
2130
2131
2132
2133
2134
2135
2136
2137
2138
2139
2140
2141
2142
2143
2144
2145
2146
2147
2148
2149
2150
2151
2152
2153
2154
2155
2156
2157
2158
2159
2160
2161
2162
2163
2164
2165
2166
2167
2168
2169
2170
2171
2172
2173
2174
2175
2176
2177
2178
2179
2180
2181
2182
2183
2184
2185
2186
2187
2188
2189
2190
2191
2192
2193
2194
2195
2196
2197
2198
2199
2200
2201
2202
2203
2204
2205
2206
2207
2208
2209
2210
2211
2212
2213
2214
2215
2216
2217
2218
2219
2220
2221
2222
2223
2224
2225
2226
2227
2228
2229
2230
2231
2232
2233
2234
2235
2236
2237
2238
2239
2240
2241
2242
2243
2244
2245
2246
2247
2248
2249
2250
2251
2252
2253
2254
2255
2256
2257
2258
2259
2260
2261
2262
2263
2264
2265
2266
2267
2268
2269
2270
2271
2272
2273
2274
2275
2276
2277
2278
2279
2280
2281
2282
2283
2284
2285
2286
2287
2288
2289
2290
2291
2292
2293
2294
2295
2296
2297
2298
2299
2300
2301
2302
2303
2304
2305
2306
2307
2308
2309
2310
2311
2312
2313
2314
2315
2316
2317
2318
2319
2320
2321
2322
2323
2324
2325
2326
2327
2328
2329
2330
2331
2332
2333
2334
2335
2336
2337
2338
2339
2340
2341
2342
2343
2344
2345
2346
2347
2348
2349
2350
2351
2352
2353
2354
2355
2356
2357
2358
2359
2360
2361
2362
2363
2364
2365
2366
2367
2368
2369
2370
2371
2372
2373
2374
2375
2376
2377
2378
2379
2380
2381
2382
2383
2384
2385
2386
2387
2388
2389
2390
2391
2392
2393
2394
2395
2396
2397
2398
2399
2400
2401
2402
2403
2404
2405
2406
2407
2408
2409
2410
2411
2412
2413
2414
2415
2416
2417
2418
2419
2420
2421
2422
2423
2424
2425
2426
2427
2428
2429
2430
2431
2432
2433
2434
2435
2436
2437
2438
2439
2440
2441
2442
2443
2444
2445
2446
2447
2448
2449
2450
2451
2452
2453
2454
2455
2456
2457
2458
2459
2460
2461
2462
2463
2464
2465
2466
2467
2468
2469
2470
2471
2472
2473
2474
2475
2476
2477
2478
2479
2480
2481
2482
2483
2484
2485
2486
2487
2488
2489
2490
2491
2492
2493
2494
2495
2496
2497
2498
2499
2500
2501
2502
2503
2504
2505
2506
2507
2508
2509
2510
2511
2512
2513
2514
2515
2516
2517
2518
2519
2520
2521
2522
2523
2524
2525
2526
2527
2528
2529
2530
2531
2532
2533
2534
2535
2536
2537
2538
2539
2540
2541
2542
2543
2544
2545
2546
2547
2548
2549
2550
2551
2552
2553
2554
2555
2556
2557
2558
2559
2560
2561
2562
2563
2564
2565
2566
2567
2568
2569
2570
2571
2572
2573
2574
2575
2576
2577
2578
2579
2580
2581
2582
2583
2584
2585
2586
2587
2588
2589
2590
2591
2592
2593
2594
2595
2596
2597
2598
2599
2600
2601
2602
2603
2604
2605
2606
2607
2608
2609
2610
2611
2612
2613
2614
2615
2616
2617
2618
2619
2620
2621
2622
2623
2624
2625
2626
2627
2628