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