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