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