| Home | Trees | Indices | Help |
|
|---|
|
|
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 sys
9 import re as regex
10 import logging
11
12
13 import wx
14
15
16 if __name__ == '__main__':
17 sys.path.insert(0, '../../')
18 from Gnumed.pycommon import gmI18N, gmCfg, gmPG2, gmMimeLib, gmExceptions, gmMatchProvider, gmDispatcher, gmDateTime, gmTools, gmShellAPI, gmHooks
19 from Gnumed.business import gmPerson
20 from Gnumed.business import gmStaff
21 from Gnumed.business import gmDocuments
22 from Gnumed.business import gmEMRStructItems
23 from Gnumed.business import gmSurgery
24
25 from Gnumed.wxpython import gmGuiHelpers
26 from Gnumed.wxpython import gmRegetMixin
27 from Gnumed.wxpython import gmPhraseWheel
28 from Gnumed.wxpython import gmPlugin
29 from Gnumed.wxpython import gmEMRStructWidgets
30 from Gnumed.wxpython import gmListWidgets
31
32
33 _log = logging.getLogger('gm.ui')
34 _log.info(__version__)
35
36
37 default_chunksize = 1 * 1024 * 1024 # 1 MB
38 #============================================================
40
41 #-----------------------------------
42 def delete_item(item):
43 doit = gmGuiHelpers.gm_show_question (
44 _( 'Are you sure you want to delete this\n'
45 'description from the document ?\n'
46 ),
47 _('Deleting document description')
48 )
49 if not doit:
50 return True
51
52 document.delete_description(pk = item[0])
53 return True
54 #-----------------------------------
55 def add_item():
56 dlg = gmGuiHelpers.cMultilineTextEntryDlg (
57 parent,
58 -1,
59 title = _('Adding document description'),
60 msg = _('Below you can add a document description.\n')
61 )
62 result = dlg.ShowModal()
63 if result == wx.ID_SAVE:
64 document.add_description(dlg.value)
65
66 dlg.Destroy()
67 return True
68 #-----------------------------------
69 def edit_item(item):
70 dlg = gmGuiHelpers.cMultilineTextEntryDlg (
71 parent,
72 -1,
73 title = _('Editing document description'),
74 msg = _('Below you can edit the document description.\n'),
75 text = item[1]
76 )
77 result = dlg.ShowModal()
78 if result == wx.ID_SAVE:
79 document.update_description(pk = item[0], description = dlg.value)
80
81 dlg.Destroy()
82 return True
83 #-----------------------------------
84 def refresh_list(lctrl):
85 descriptions = document.get_descriptions()
86
87 lctrl.set_string_items(items = [
88 u'%s%s' % ( (u' '.join(regex.split('\r\n+|\r+|\n+|\t+', desc[1])))[:30], gmTools.u_ellipsis )
89 for desc in descriptions
90 ])
91 lctrl.set_data(data = descriptions)
92 #-----------------------------------
93
94 gmListWidgets.get_choices_from_list (
95 parent = parent,
96 msg = _('Select the description you are interested in.\n'),
97 caption = _('Managing document descriptions'),
98 columns = [_('Description')],
99 edit_callback = edit_item,
100 new_callback = add_item,
101 delete_callback = delete_item,
102 refresh_callback = refresh_list,
103 single_selection = True,
104 can_return_empty = True
105 )
106
107 return True
108 #============================================================
110 try:
111 del kwargs['signal']
112 del kwargs['sender']
113 except KeyError:
114 pass
115 wx.CallAfter(save_file_as_new_document, **kwargs)
116
118 try:
119 del kwargs['signal']
120 del kwargs['sender']
121 except KeyError:
122 pass
123 wx.CallAfter(save_files_as_new_document, **kwargs)
124 #----------------------
125 -def save_file_as_new_document(parent=None, filename=None, document_type=None, unlock_patient=False, episode=None, review_as_normal=False):
126 return save_files_as_new_document (
127 parent = parent,
128 filenames = [filename],
129 document_type = document_type,
130 unlock_patient = unlock_patient,
131 episode = episode,
132 review_as_normal = review_as_normal
133 )
134 #----------------------
135 -def save_files_as_new_document(parent=None, filenames=None, document_type=None, unlock_patient=False, episode=None, review_as_normal=False):
136
137 pat = gmPerson.gmCurrentPatient()
138 if not pat.connected:
139 return None
140
141 emr = pat.get_emr()
142
143 if parent is None:
144 parent = wx.GetApp().GetTopWindow()
145
146 if episode is None:
147 all_epis = emr.get_episodes()
148 # FIXME: what to do here ? probably create dummy episode
149 if len(all_epis) == 0:
150 episode = emr.add_episode(episode_name = _('Documents'), is_open = False)
151 else:
152 dlg = gmEMRStructWidgets.cEpisodeListSelectorDlg(parent = parent, id = -1, episodes = all_epis)
153 dlg.SetTitle(_('Select the episode under which to file the document ...'))
154 btn_pressed = dlg.ShowModal()
155 episode = dlg.get_selected_item_data(only_one = True)
156 dlg.Destroy()
157
158 if (btn_pressed == wx.ID_CANCEL) or (episode is None):
159 if unlock_patient:
160 pat.locked = False
161 return None
162
163 doc_type = gmDocuments.create_document_type(document_type = document_type)
164
165 docs_folder = pat.get_document_folder()
166 doc = docs_folder.add_document (
167 document_type = doc_type['pk_doc_type'],
168 encounter = emr.active_encounter['pk_encounter'],
169 episode = episode['pk_episode']
170 )
171 doc.add_parts_from_files(files = filenames)
172
173 if review_as_normal:
174 doc.set_reviewed(technically_abnormal = False, clinically_relevant = False)
175
176 if unlock_patient:
177 pat.locked = False
178
179 gmDispatcher.send(signal = 'statustext', msg = _('Imported new document from %s.') % filenames, beep = True)
180
181 return doc
182 #----------------------
183 gmDispatcher.connect(signal = u'import_document_from_file', receiver = _save_file_as_new_document)
184 gmDispatcher.connect(signal = u'import_document_from_files', receiver = _save_files_as_new_document)
185 #============================================================
187 """Let user select a document comment from all existing comments."""
189
190 gmPhraseWheel.cPhraseWheel.__init__(self, *args, **kwargs)
191
192 context = {
193 u'ctxt_doc_type': {
194 u'where_part': u'and fk_type = %(pk_doc_type)s',
195 u'placeholder': u'pk_doc_type'
196 }
197 }
198
199 mp = gmMatchProvider.cMatchProvider_SQL2 (
200 queries = [u"""
201 SELECT
202 data,
203 field_label,
204 list_label
205 FROM (
206 SELECT DISTINCT ON (field_label) *
207 FROM (
208 -- constrained by doc type
209 SELECT
210 comment AS data,
211 comment AS field_label,
212 comment AS list_label,
213 1 AS rank
214 FROM blobs.doc_med
215 WHERE
216 comment %(fragment_condition)s
217 %(ctxt_doc_type)s
218
219 UNION ALL
220
221 SELECT
222 comment AS data,
223 comment AS field_label,
224 comment AS list_label,
225 2 AS rank
226 FROM blobs.doc_med
227 WHERE
228 comment %(fragment_condition)s
229 ) AS q_union
230 ) AS q_distinct
231 ORDER BY rank, list_label
232 LIMIT 25"""],
233 context = context
234 )
235 mp.setThresholds(3, 5, 7)
236 mp.unset_context(u'pk_doc_type')
237
238 self.matcher = mp
239 self.picklist_delay = 50
240
241 self.SetToolTipString(_('Enter a comment on the document.'))
242 #============================================================
243 # document type widgets
244 #============================================================
246
247 if parent is None:
248 parent = wx.GetApp().GetTopWindow()
249
250 dlg = cEditDocumentTypesDlg(parent = parent)
251 dlg.ShowModal()
252 #============================================================
253 from Gnumed.wxGladeWidgets import wxgEditDocumentTypesDlg
254
260
261 #============================================================
262 from Gnumed.wxGladeWidgets import wxgEditDocumentTypesPnl
263
265 """A panel grouping together fields to edit the list of document types."""
266
268 wxgEditDocumentTypesPnl.wxgEditDocumentTypesPnl.__init__(self, *args, **kwargs)
269 self.__init_ui()
270 self.__register_interests()
271 self.repopulate_ui()
272 #--------------------------------------------------------
274 self._LCTRL_doc_type.set_columns([_('Type'), _('Translation'), _('User defined'), _('In use')])
275 self._LCTRL_doc_type.set_column_widths()
276 #--------------------------------------------------------
279 #--------------------------------------------------------
281 wx.CallAfter(self.repopulate_ui)
282 #--------------------------------------------------------
284
285 self._LCTRL_doc_type.DeleteAllItems()
286
287 doc_types = gmDocuments.get_document_types()
288 pos = len(doc_types) + 1
289
290 for doc_type in doc_types:
291 row_num = self._LCTRL_doc_type.InsertStringItem(pos, label = doc_type['type'])
292 self._LCTRL_doc_type.SetStringItem(index = row_num, col = 1, label = doc_type['l10n_type'])
293 if doc_type['is_user_defined']:
294 self._LCTRL_doc_type.SetStringItem(index = row_num, col = 2, label = ' X ')
295 if doc_type['is_in_use']:
296 self._LCTRL_doc_type.SetStringItem(index = row_num, col = 3, label = ' X ')
297
298 if len(doc_types) > 0:
299 self._LCTRL_doc_type.set_data(data = doc_types)
300 self._LCTRL_doc_type.SetColumnWidth(col=0, width=wx.LIST_AUTOSIZE)
301 self._LCTRL_doc_type.SetColumnWidth(col=1, width=wx.LIST_AUTOSIZE)
302 self._LCTRL_doc_type.SetColumnWidth(col=2, width=wx.LIST_AUTOSIZE_USEHEADER)
303 self._LCTRL_doc_type.SetColumnWidth(col=3, width=wx.LIST_AUTOSIZE_USEHEADER)
304
305 self._TCTRL_type.SetValue('')
306 self._TCTRL_l10n_type.SetValue('')
307
308 self._BTN_set_translation.Enable(False)
309 self._BTN_delete.Enable(False)
310 self._BTN_add.Enable(False)
311 self._BTN_reassign.Enable(False)
312
313 self._LCTRL_doc_type.SetFocus()
314 #--------------------------------------------------------
315 # event handlers
316 #--------------------------------------------------------
318 doc_type = self._LCTRL_doc_type.get_selected_item_data()
319
320 self._TCTRL_type.SetValue(doc_type['type'])
321 self._TCTRL_l10n_type.SetValue(doc_type['l10n_type'])
322
323 self._BTN_set_translation.Enable(True)
324 self._BTN_delete.Enable(not bool(doc_type['is_in_use']))
325 self._BTN_add.Enable(False)
326 self._BTN_reassign.Enable(True)
327
328 return
329 #--------------------------------------------------------
331 self._BTN_set_translation.Enable(False)
332 self._BTN_delete.Enable(False)
333 self._BTN_reassign.Enable(False)
334
335 self._BTN_add.Enable(True)
336 # self._LCTRL_doc_type.deselect_selected_item()
337 return
338 #--------------------------------------------------------
345 #--------------------------------------------------------
362 #--------------------------------------------------------
372 #--------------------------------------------------------
404 #============================================================
406 """Let user select a document type."""
408
409 gmPhraseWheel.cPhraseWheel.__init__(self, *args, **kwargs)
410
411 mp = gmMatchProvider.cMatchProvider_SQL2 (
412 queries = [
413 u"""SELECT
414 data,
415 field_label,
416 list_label
417 FROM ((
418 SELECT
419 pk_doc_type AS data,
420 l10n_type AS field_label,
421 l10n_type AS list_label,
422 1 AS rank
423 FROM blobs.v_doc_type
424 WHERE
425 is_user_defined IS True
426 AND
427 l10n_type %(fragment_condition)s
428 ) UNION (
429 SELECT
430 pk_doc_type AS data,
431 l10n_type AS field_label,
432 l10n_type AS list_label,
433 2 AS rank
434 FROM blobs.v_doc_type
435 WHERE
436 is_user_defined IS False
437 AND
438 l10n_type %(fragment_condition)s
439 )) AS q1
440 ORDER BY q1.rank, q1.list_label"""]
441 )
442 mp.setThresholds(2, 4, 6)
443
444 self.matcher = mp
445 self.picklist_delay = 50
446
447 self.SetToolTipString(_('Select the document type.'))
448 #--------------------------------------------------------
450
451 doc_type = self.GetValue().strip()
452 if doc_type == u'':
453 gmDispatcher.send(signal = u'statustext', msg = _('Cannot create document type without name.'), beep = True)
454 _log.debug('cannot create document type without name')
455 return
456
457 pk = gmDocuments.create_document_type(doc_type)['pk_doc_type']
458 if pk is None:
459 self.data = {}
460 else:
461 self.SetText (
462 value = doc_type,
463 data = pk
464 )
465 #============================================================
466 # document review widgets
467 #============================================================
469 if parent is None:
470 parent = wx.GetApp().GetTopWindow()
471 dlg = cReviewDocPartDlg (
472 parent = parent,
473 id = -1,
474 part = part
475 )
476 dlg.ShowModal()
477 dlg.Destroy()
478 #------------------------------------------------------------
480 return review_document_part(parent = parent, part = document)
481 #------------------------------------------------------------
482 from Gnumed.wxGladeWidgets import wxgReviewDocPartDlg
483
486 """Support parts and docs now.
487 """
488 part = kwds['part']
489 del kwds['part']
490 wxgReviewDocPartDlg.wxgReviewDocPartDlg.__init__(self, *args, **kwds)
491
492 if isinstance(part, gmDocuments.cDocumentPart):
493 self.__part = part
494 self.__doc = self.__part.get_containing_document()
495 self.__reviewing_doc = False
496 elif isinstance(part, gmDocuments.cDocument):
497 self.__doc = part
498 if len(self.__doc.parts) == 0:
499 self.__part = None
500 else:
501 self.__part = self.__doc.parts[0]
502 self.__reviewing_doc = True
503 else:
504 raise ValueError('<part> must be gmDocuments.cDocument or gmDocuments.cDocumentPart instance, got <%s>' % type(part))
505
506 self.__init_ui_data()
507 #--------------------------------------------------------
508 # internal API
509 #--------------------------------------------------------
511 # FIXME: fix this
512 # associated episode (add " " to avoid popping up pick list)
513 self._PhWheel_episode.SetText('%s ' % self.__doc['episode'], self.__doc['pk_episode'])
514 self._PhWheel_doc_type.SetText(value = self.__doc['l10n_type'], data = self.__doc['pk_type'])
515 self._PhWheel_doc_type.add_callback_on_set_focus(self._on_doc_type_gets_focus)
516 self._PhWheel_doc_type.add_callback_on_lose_focus(self._on_doc_type_loses_focus)
517
518 if self.__reviewing_doc:
519 self._PRW_doc_comment.SetText(gmTools.coalesce(self.__doc['comment'], ''))
520 self._PRW_doc_comment.set_context(context = 'pk_doc_type', val = self.__doc['pk_type'])
521 else:
522 self._PRW_doc_comment.SetText(gmTools.coalesce(self.__part['obj_comment'], ''))
523
524 fts = gmDateTime.cFuzzyTimestamp(timestamp = self.__doc['clin_when'])
525 self._PhWheel_doc_date.SetText(fts.strftime('%Y-%m-%d'), fts)
526 self._TCTRL_reference.SetValue(gmTools.coalesce(self.__doc['ext_ref'], ''))
527 if self.__reviewing_doc:
528 self._TCTRL_filename.Enable(False)
529 self._SPINCTRL_seq_idx.Enable(False)
530 else:
531 self._TCTRL_filename.SetValue(gmTools.coalesce(self.__part['filename'], ''))
532 self._SPINCTRL_seq_idx.SetValue(gmTools.coalesce(self.__part['seq_idx'], 0))
533
534 self._LCTRL_existing_reviews.InsertColumn(0, _('who'))
535 self._LCTRL_existing_reviews.InsertColumn(1, _('when'))
536 self._LCTRL_existing_reviews.InsertColumn(2, _('+/-'))
537 self._LCTRL_existing_reviews.InsertColumn(3, _('!'))
538 self._LCTRL_existing_reviews.InsertColumn(4, _('comment'))
539
540 self.__reload_existing_reviews()
541
542 if self._LCTRL_existing_reviews.GetItemCount() > 0:
543 self._LCTRL_existing_reviews.SetColumnWidth(col=0, width=wx.LIST_AUTOSIZE)
544 self._LCTRL_existing_reviews.SetColumnWidth(col=1, width=wx.LIST_AUTOSIZE)
545 self._LCTRL_existing_reviews.SetColumnWidth(col=2, width=wx.LIST_AUTOSIZE_USEHEADER)
546 self._LCTRL_existing_reviews.SetColumnWidth(col=3, width=wx.LIST_AUTOSIZE_USEHEADER)
547 self._LCTRL_existing_reviews.SetColumnWidth(col=4, width=wx.LIST_AUTOSIZE)
548
549 if self.__part is None:
550 self._ChBOX_review.SetValue(False)
551 self._ChBOX_review.Enable(False)
552 self._ChBOX_abnormal.Enable(False)
553 self._ChBOX_relevant.Enable(False)
554 self._ChBOX_sign_all_pages.Enable(False)
555 else:
556 me = gmStaff.gmCurrentProvider()
557 if self.__part['pk_intended_reviewer'] == me['pk_staff']:
558 msg = _('(you are the primary reviewer)')
559 else:
560 msg = _('(someone else is the primary reviewer)')
561 self._TCTRL_responsible.SetValue(msg)
562 # init my review if any
563 if self.__part['reviewed_by_you']:
564 revs = self.__part.get_reviews()
565 for rev in revs:
566 if rev['is_your_review']:
567 self._ChBOX_abnormal.SetValue(bool(rev[2]))
568 self._ChBOX_relevant.SetValue(bool(rev[3]))
569 break
570
571 self._ChBOX_sign_all_pages.SetValue(self.__reviewing_doc)
572
573 return True
574 #--------------------------------------------------------
576 self._LCTRL_existing_reviews.DeleteAllItems()
577 if self.__part is None:
578 return True
579 revs = self.__part.get_reviews() # FIXME: this is ugly as sin, it should be dicts, not lists
580 if len(revs) == 0:
581 return True
582 # find special reviews
583 review_by_responsible_doc = None
584 reviews_by_others = []
585 for rev in revs:
586 if rev['is_review_by_responsible_reviewer'] and not rev['is_your_review']:
587 review_by_responsible_doc = rev
588 if not (rev['is_review_by_responsible_reviewer'] or rev['is_your_review']):
589 reviews_by_others.append(rev)
590 # display them
591 if review_by_responsible_doc is not None:
592 row_num = self._LCTRL_existing_reviews.InsertStringItem(sys.maxint, label=review_by_responsible_doc[0])
593 self._LCTRL_existing_reviews.SetItemTextColour(row_num, col=wx.BLUE)
594 self._LCTRL_existing_reviews.SetStringItem(index = row_num, col=0, label=review_by_responsible_doc[0])
595 self._LCTRL_existing_reviews.SetStringItem(index = row_num, col=1, label=review_by_responsible_doc[1].strftime('%x %H:%M'))
596 if review_by_responsible_doc['is_technically_abnormal']:
597 self._LCTRL_existing_reviews.SetStringItem(index = row_num, col=2, label=u'X')
598 if review_by_responsible_doc['clinically_relevant']:
599 self._LCTRL_existing_reviews.SetStringItem(index = row_num, col=3, label=u'X')
600 self._LCTRL_existing_reviews.SetStringItem(index = row_num, col=4, label=review_by_responsible_doc[6])
601 row_num += 1
602 for rev in reviews_by_others:
603 row_num = self._LCTRL_existing_reviews.InsertStringItem(sys.maxint, label=rev[0])
604 self._LCTRL_existing_reviews.SetStringItem(index = row_num, col=0, label=rev[0])
605 self._LCTRL_existing_reviews.SetStringItem(index = row_num, col=1, label=rev[1].strftime('%x %H:%M'))
606 if rev['is_technically_abnormal']:
607 self._LCTRL_existing_reviews.SetStringItem(index = row_num, col=2, label=u'X')
608 if rev['clinically_relevant']:
609 self._LCTRL_existing_reviews.SetStringItem(index = row_num, col=3, label=u'X')
610 self._LCTRL_existing_reviews.SetStringItem(index = row_num, col=4, label=rev[6])
611 return True
612 #--------------------------------------------------------
613 # event handlers
614 #--------------------------------------------------------
702 #--------------------------------------------------------
704 state = self._ChBOX_review.GetValue()
705 self._ChBOX_abnormal.Enable(enable = state)
706 self._ChBOX_relevant.Enable(enable = state)
707 self._ChBOX_responsible.Enable(enable = state)
708 #--------------------------------------------------------
710 """Per Jim: Changing the doc type happens a lot more often
711 then correcting spelling, hence select-all on getting focus.
712 """
713 self._PhWheel_doc_type.SetSelection(-1, -1)
714 #--------------------------------------------------------
716 pk_doc_type = self._PhWheel_doc_type.GetData()
717 if pk_doc_type is None:
718 self._PRW_doc_comment.unset_context(context = 'pk_doc_type')
719 else:
720 self._PRW_doc_comment.set_context(context = 'pk_doc_type', val = pk_doc_type)
721 return True
722 #============================================================
724
725 _log.debug('acquiring images from [%s]', device)
726
727 # do not import globally since we might want to use
728 # this module without requiring any scanner to be available
729 from Gnumed.pycommon import gmScanBackend
730 try:
731 fnames = gmScanBackend.acquire_pages_into_files (
732 device = device,
733 delay = 5,
734 calling_window = calling_window
735 )
736 except OSError:
737 _log.exception('problem acquiring image from source')
738 gmGuiHelpers.gm_show_error (
739 aMessage = _(
740 'No images could be acquired from the source.\n\n'
741 'This may mean the scanner driver is not properly installed.\n\n'
742 'On Windows you must install the TWAIN Python module\n'
743 'while on Linux and MacOSX it is recommended to install\n'
744 'the XSane package.'
745 ),
746 aTitle = _('Acquiring images')
747 )
748 return None
749
750 _log.debug('acquired %s images', len(fnames))
751
752 return fnames
753 #------------------------------------------------------------
754 from Gnumed.wxGladeWidgets import wxgScanIdxPnl
755
758 wxgScanIdxPnl.wxgScanIdxPnl.__init__(self, *args, **kwds)
759 gmPlugin.cPatientChange_PluginMixin.__init__(self)
760
761 self._PhWheel_reviewer.matcher = gmPerson.cMatchProvider_Provider()
762
763 self.__init_ui_data()
764 self._PhWheel_doc_type.add_callback_on_lose_focus(self._on_doc_type_loses_focus)
765
766 # make me and listctrl a file drop target
767 dt = gmGuiHelpers.cFileDropTarget(self)
768 self.SetDropTarget(dt)
769 dt = gmGuiHelpers.cFileDropTarget(self._LBOX_doc_pages)
770 self._LBOX_doc_pages.SetDropTarget(dt)
771 self._LBOX_doc_pages.add_filenames = self.add_filenames_to_listbox
772
773 # do not import globally since we might want to use
774 # this module without requiring any scanner to be available
775 from Gnumed.pycommon import gmScanBackend
776 self.scan_module = gmScanBackend
777 #--------------------------------------------------------
778 # file drop target API
779 #--------------------------------------------------------
781 self.add_filenames(filenames=filenames)
782 #--------------------------------------------------------
784 pat = gmPerson.gmCurrentPatient()
785 if not pat.connected:
786 gmDispatcher.send(signal='statustext', msg=_('Cannot accept new documents. No active patient.'))
787 return
788
789 # dive into folders dropped onto us and extract files (one level deep only)
790 real_filenames = []
791 for pathname in filenames:
792 try:
793 files = os.listdir(pathname)
794 gmDispatcher.send(signal='statustext', msg=_('Extracting files from folder [%s] ...') % pathname)
795 for file in files:
796 fullname = os.path.join(pathname, file)
797 if not os.path.isfile(fullname):
798 continue
799 real_filenames.append(fullname)
800 except OSError:
801 real_filenames.append(pathname)
802
803 self.acquired_pages.extend(real_filenames)
804 self.__reload_LBOX_doc_pages()
805 #--------------------------------------------------------
808 #--------------------------------------------------------
809 # patient change plugin API
810 #--------------------------------------------------------
814 #--------------------------------------------------------
817 #--------------------------------------------------------
818 # internal API
819 #--------------------------------------------------------
821 # -----------------------------
822 self._PhWheel_episode.SetText(value = _('other documents'), suppress_smarts = True)
823 self._PhWheel_doc_type.SetText('')
824 # -----------------------------
825 # FIXME: make this configurable: either now() or last_date()
826 fts = gmDateTime.cFuzzyTimestamp()
827 self._PhWheel_doc_date.SetText(fts.strftime('%Y-%m-%d'), fts)
828 self._PRW_doc_comment.SetText('')
829 # FIXME: should be set to patient's primary doc
830 self._PhWheel_reviewer.selection_only = True
831 me = gmStaff.gmCurrentProvider()
832 self._PhWheel_reviewer.SetText (
833 value = u'%s (%s%s %s)' % (me['short_alias'], me['title'], me['firstnames'], me['lastnames']),
834 data = me['pk_staff']
835 )
836 # -----------------------------
837 # FIXME: set from config item
838 self._ChBOX_reviewed.SetValue(False)
839 self._ChBOX_abnormal.Disable()
840 self._ChBOX_abnormal.SetValue(False)
841 self._ChBOX_relevant.Disable()
842 self._ChBOX_relevant.SetValue(False)
843 # -----------------------------
844 self._TBOX_description.SetValue('')
845 # -----------------------------
846 # the list holding our page files
847 self._LBOX_doc_pages.Clear()
848 self.acquired_pages = []
849
850 self._PhWheel_doc_type.SetFocus()
851 #--------------------------------------------------------
853 self._LBOX_doc_pages.Clear()
854 if len(self.acquired_pages) > 0:
855 for i in range(len(self.acquired_pages)):
856 fname = self.acquired_pages[i]
857 self._LBOX_doc_pages.Append(_('part %s: %s') % (i+1, fname), fname)
858 #--------------------------------------------------------
860 title = _('saving document')
861
862 if self.acquired_pages is None or len(self.acquired_pages) == 0:
863 dbcfg = gmCfg.cCfgSQL()
864 allow_empty = bool(dbcfg.get2 (
865 option = u'horstspace.scan_index.allow_partless_documents',
866 workplace = gmSurgery.gmCurrentPractice().active_workplace,
867 bias = 'user',
868 default = False
869 ))
870 if allow_empty:
871 save_empty = gmGuiHelpers.gm_show_question (
872 aMessage = _('No parts to save. Really save an empty document as a reference ?'),
873 aTitle = title
874 )
875 if not save_empty:
876 return False
877 else:
878 gmGuiHelpers.gm_show_error (
879 aMessage = _('No parts to save. Aquire some parts first.'),
880 aTitle = title
881 )
882 return False
883
884 doc_type_pk = self._PhWheel_doc_type.GetData(can_create = True)
885 if doc_type_pk is None:
886 gmGuiHelpers.gm_show_error (
887 aMessage = _('No document type applied. Choose a document type'),
888 aTitle = title
889 )
890 return False
891
892 # this should be optional, actually
893 # if self._PRW_doc_comment.GetValue().strip() == '':
894 # gmGuiHelpers.gm_show_error (
895 # aMessage = _('No document comment supplied. Add a comment for this document.'),
896 # aTitle = title
897 # )
898 # return False
899
900 if self._PhWheel_episode.GetValue().strip() == '':
901 gmGuiHelpers.gm_show_error (
902 aMessage = _('You must select an episode to save this document under.'),
903 aTitle = title
904 )
905 return False
906
907 if self._PhWheel_reviewer.GetData() is None:
908 gmGuiHelpers.gm_show_error (
909 aMessage = _('You need to select from the list of staff members the doctor who is intended to sign the document.'),
910 aTitle = title
911 )
912 return False
913
914 return True
915 #--------------------------------------------------------
917
918 if not reconfigure:
919 dbcfg = gmCfg.cCfgSQL()
920 device = dbcfg.get2 (
921 option = 'external.xsane.default_device',
922 workplace = gmSurgery.gmCurrentPractice().active_workplace,
923 bias = 'workplace',
924 default = ''
925 )
926 if device.strip() == u'':
927 device = None
928 if device is not None:
929 return device
930
931 try:
932 devices = self.scan_module.get_devices()
933 except:
934 _log.exception('cannot retrieve list of image sources')
935 gmDispatcher.send(signal = 'statustext', msg = _('There is no scanner support installed on this machine.'))
936 return None
937
938 if devices is None:
939 # get_devices() not implemented for TWAIN yet
940 # XSane has its own chooser (so does TWAIN)
941 return None
942
943 if len(devices) == 0:
944 gmDispatcher.send(signal = 'statustext', msg = _('Cannot find an active scanner.'))
945 return None
946
947 # device_names = []
948 # for device in devices:
949 # device_names.append('%s (%s)' % (device[2], device[0]))
950
951 device = gmListWidgets.get_choices_from_list (
952 parent = self,
953 msg = _('Select an image capture device'),
954 caption = _('device selection'),
955 choices = [ '%s (%s)' % (d[2], d[0]) for d in devices ],
956 columns = [_('Device')],
957 data = devices,
958 single_selection = True
959 )
960 if device is None:
961 return None
962
963 # FIXME: add support for actually reconfiguring
964 return device[0]
965 #--------------------------------------------------------
966 # event handling API
967 #--------------------------------------------------------
969
970 chosen_device = self.get_device_to_use()
971
972 tmpdir = os.path.expanduser(os.path.join('~', '.gnumed', 'tmp'))
973 try:
974 gmTools.mkdir(tmpdir)
975 except:
976 tmpdir = None
977
978 # FIXME: configure whether to use XSane or sane directly
979 # FIXME: add support for xsane_device_settings argument
980 try:
981 fnames = self.scan_module.acquire_pages_into_files (
982 device = chosen_device,
983 delay = 5,
984 tmpdir = tmpdir,
985 calling_window = self
986 )
987 except OSError:
988 _log.exception('problem acquiring image from source')
989 gmGuiHelpers.gm_show_error (
990 aMessage = _(
991 'No pages could be acquired from the source.\n\n'
992 'This may mean the scanner driver is not properly installed.\n\n'
993 'On Windows you must install the TWAIN Python module\n'
994 'while on Linux and MacOSX it is recommended to install\n'
995 'the XSane package.'
996 ),
997 aTitle = _('acquiring page')
998 )
999 return None
1000
1001 if len(fnames) == 0: # no pages scanned
1002 return True
1003
1004 self.acquired_pages.extend(fnames)
1005 self.__reload_LBOX_doc_pages()
1006
1007 return True
1008 #--------------------------------------------------------
1010 # patient file chooser
1011 dlg = wx.FileDialog (
1012 parent = None,
1013 message = _('Choose a file'),
1014 defaultDir = os.path.expanduser(os.path.join('~', 'gnumed')),
1015 defaultFile = '',
1016 wildcard = "%s (*)|*|TIFFs (*.tif)|*.tif|JPEGs (*.jpg)|*.jpg|%s (*.*)|*.*" % (_('all files'), _('all files (Win)')),
1017 style = wx.OPEN | wx.HIDE_READONLY | wx.FILE_MUST_EXIST | wx.MULTIPLE
1018 )
1019 result = dlg.ShowModal()
1020 if result != wx.ID_CANCEL:
1021 files = dlg.GetPaths()
1022 for file in files:
1023 self.acquired_pages.append(file)
1024 self.__reload_LBOX_doc_pages()
1025 dlg.Destroy()
1026 #--------------------------------------------------------
1028 # did user select a page ?
1029 page_idx = self._LBOX_doc_pages.GetSelection()
1030 if page_idx == -1:
1031 gmGuiHelpers.gm_show_info (
1032 aMessage = _('You must select a part before you can view it.'),
1033 aTitle = _('displaying part')
1034 )
1035 return None
1036 # now, which file was that again ?
1037 page_fname = self._LBOX_doc_pages.GetClientData(page_idx)
1038
1039 (result, msg) = gmMimeLib.call_viewer_on_file(page_fname)
1040 if not result:
1041 gmGuiHelpers.gm_show_warning (
1042 aMessage = _('Cannot display document part:\n%s') % msg,
1043 aTitle = _('displaying part')
1044 )
1045 return None
1046 return 1
1047 #--------------------------------------------------------
1049 page_idx = self._LBOX_doc_pages.GetSelection()
1050 if page_idx == -1:
1051 gmGuiHelpers.gm_show_info (
1052 aMessage = _('You must select a part before you can delete it.'),
1053 aTitle = _('deleting part')
1054 )
1055 return None
1056 page_fname = self._LBOX_doc_pages.GetClientData(page_idx)
1057
1058 # 1) del item from self.acquired_pages
1059 self.acquired_pages[page_idx:(page_idx+1)] = []
1060
1061 # 2) reload list box
1062 self.__reload_LBOX_doc_pages()
1063
1064 # 3) optionally kill file in the file system
1065 do_delete = gmGuiHelpers.gm_show_question (
1066 _('The part has successfully been removed from the document.\n'
1067 '\n'
1068 'Do you also want to permanently delete the file\n'
1069 '\n'
1070 ' [%s]\n'
1071 '\n'
1072 'from which this document part was loaded ?\n'
1073 '\n'
1074 'If it is a temporary file for a page you just scanned\n'
1075 'this makes a lot of sense. In other cases you may not\n'
1076 'want to lose the file.\n'
1077 '\n'
1078 'Pressing [YES] will permanently remove the file\n'
1079 'from your computer.\n'
1080 ) % page_fname,
1081 _('Removing document part')
1082 )
1083 if do_delete:
1084 try:
1085 os.remove(page_fname)
1086 except:
1087 _log.exception('Error deleting file.')
1088 gmGuiHelpers.gm_show_error (
1089 aMessage = _('Cannot delete part in file [%s].\n\nYou may not have write access to it.') % page_fname,
1090 aTitle = _('deleting part')
1091 )
1092
1093 return 1
1094 #--------------------------------------------------------
1096
1097 if not self.__valid_for_save():
1098 return False
1099
1100 wx.BeginBusyCursor()
1101
1102 pat = gmPerson.gmCurrentPatient()
1103 doc_folder = pat.get_document_folder()
1104 emr = pat.get_emr()
1105
1106 # create new document
1107 pk_episode = self._PhWheel_episode.GetData()
1108 if pk_episode is None:
1109 episode = emr.add_episode (
1110 episode_name = self._PhWheel_episode.GetValue().strip(),
1111 is_open = True
1112 )
1113 if episode is None:
1114 wx.EndBusyCursor()
1115 gmGuiHelpers.gm_show_error (
1116 aMessage = _('Cannot start episode [%s].') % self._PhWheel_episode.GetValue().strip(),
1117 aTitle = _('saving document')
1118 )
1119 return False
1120 pk_episode = episode['pk_episode']
1121
1122 encounter = emr.active_encounter['pk_encounter']
1123 document_type = self._PhWheel_doc_type.GetData()
1124 new_doc = doc_folder.add_document(document_type, encounter, pk_episode)
1125 if new_doc is None:
1126 wx.EndBusyCursor()
1127 gmGuiHelpers.gm_show_error (
1128 aMessage = _('Cannot create new document.'),
1129 aTitle = _('saving document')
1130 )
1131 return False
1132
1133 # update business object with metadata
1134 # - date of generation
1135 new_doc['clin_when'] = self._PhWheel_doc_date.GetData().get_pydt()
1136 # - external reference
1137 cfg = gmCfg.cCfgSQL()
1138 generate_uuid = bool (
1139 cfg.get2 (
1140 option = 'horstspace.scan_index.generate_doc_uuid',
1141 workplace = gmSurgery.gmCurrentPractice().active_workplace,
1142 bias = 'user',
1143 default = False
1144 )
1145 )
1146 ref = None
1147 if generate_uuid:
1148 ref = gmDocuments.get_ext_ref()
1149 if ref is not None:
1150 new_doc['ext_ref'] = ref
1151 # - comment
1152 comment = self._PRW_doc_comment.GetLineText(0).strip()
1153 if comment != u'':
1154 new_doc['comment'] = comment
1155 # - save it
1156 if not new_doc.save_payload():
1157 wx.EndBusyCursor()
1158 gmGuiHelpers.gm_show_error (
1159 aMessage = _('Cannot update document metadata.'),
1160 aTitle = _('saving document')
1161 )
1162 return False
1163 # - long description
1164 description = self._TBOX_description.GetValue().strip()
1165 if description != '':
1166 if not new_doc.add_description(description):
1167 wx.EndBusyCursor()
1168 gmGuiHelpers.gm_show_error (
1169 aMessage = _('Cannot add document description.'),
1170 aTitle = _('saving document')
1171 )
1172 return False
1173
1174 # add document parts from files
1175 success, msg, filename = new_doc.add_parts_from_files (
1176 files = self.acquired_pages,
1177 reviewer = self._PhWheel_reviewer.GetData()
1178 )
1179 if not success:
1180 wx.EndBusyCursor()
1181 gmGuiHelpers.gm_show_error (
1182 aMessage = msg,
1183 aTitle = _('saving document')
1184 )
1185 return False
1186
1187 # set reviewed status
1188 if self._ChBOX_reviewed.GetValue():
1189 if not new_doc.set_reviewed (
1190 technically_abnormal = self._ChBOX_abnormal.GetValue(),
1191 clinically_relevant = self._ChBOX_relevant.GetValue()
1192 ):
1193 msg = _('Error setting "reviewed" status of new document.')
1194
1195 gmHooks.run_hook_script(hook = u'after_new_doc_created')
1196
1197 # inform user
1198 show_id = bool (
1199 cfg.get2 (
1200 option = 'horstspace.scan_index.show_doc_id',
1201 workplace = gmSurgery.gmCurrentPractice().active_workplace,
1202 bias = 'user'
1203 )
1204 )
1205 wx.EndBusyCursor()
1206 if show_id:
1207 if ref is None:
1208 msg = _('Successfully saved the new document.')
1209 else:
1210 msg = _(
1211 """The reference ID for the new document is:
1212
1213 <%s>
1214
1215 You probably want to write it down on the
1216 original documents.
1217
1218 If you don't care about the ID you can switch
1219 off this message in the GNUmed configuration.""") % ref
1220 gmGuiHelpers.gm_show_info (
1221 aMessage = msg,
1222 aTitle = _('Saving document')
1223 )
1224 else:
1225 gmDispatcher.send(signal='statustext', msg=_('Successfully saved new document.'))
1226
1227 self.__init_ui_data()
1228 return True
1229 #--------------------------------------------------------
1232 #--------------------------------------------------------
1234 self._ChBOX_abnormal.Enable(enable = self._ChBOX_reviewed.GetValue())
1235 self._ChBOX_relevant.Enable(enable = self._ChBOX_reviewed.GetValue())
1236 #--------------------------------------------------------
1238 pk_doc_type = self._PhWheel_doc_type.GetData()
1239 if pk_doc_type is None:
1240 self._PRW_doc_comment.unset_context(context = 'pk_doc_type')
1241 else:
1242 self._PRW_doc_comment.set_context(context = 'pk_doc_type', val = pk_doc_type)
1243 return True
1244 #============================================================
1246
1247 if parent is None:
1248 parent = wx.GetApp().GetTopWindow()
1249
1250 # sanity check
1251 if part['size'] == 0:
1252 _log.debug('cannot display part [%s] - 0 bytes', part['pk_obj'])
1253 gmGuiHelpers.gm_show_error (
1254 aMessage = _('Document part does not seem to exist in database !'),
1255 aTitle = _('showing document')
1256 )
1257 return None
1258
1259 wx.BeginBusyCursor()
1260 cfg = gmCfg.cCfgSQL()
1261
1262 # determine database export chunk size
1263 chunksize = int(
1264 cfg.get2 (
1265 option = "horstspace.blob_export_chunk_size",
1266 workplace = gmSurgery.gmCurrentPractice().active_workplace,
1267 bias = 'workplace',
1268 default = 2048
1269 ))
1270
1271 # shall we force blocking during view ?
1272 block_during_view = bool( cfg.get2 (
1273 option = 'horstspace.document_viewer.block_during_view',
1274 workplace = gmSurgery.gmCurrentPractice().active_workplace,
1275 bias = 'user',
1276 default = None
1277 ))
1278
1279 wx.EndBusyCursor()
1280
1281 # display it
1282 successful, msg = part.display_via_mime (
1283 chunksize = chunksize,
1284 block = block_during_view
1285 )
1286 if not successful:
1287 gmGuiHelpers.gm_show_error (
1288 aMessage = _('Cannot display document part:\n%s') % msg,
1289 aTitle = _('showing document')
1290 )
1291 return None
1292
1293 # handle review after display
1294 # 0: never
1295 # 1: always
1296 # 2: if no review by myself exists yet
1297 # 3: if no review at all exists yet
1298 # 4: if no review by responsible reviewer
1299 review_after_display = int(cfg.get2 (
1300 option = 'horstspace.document_viewer.review_after_display',
1301 workplace = gmSurgery.gmCurrentPractice().active_workplace,
1302 bias = 'user',
1303 default = 3
1304 ))
1305 if review_after_display == 1: # always review
1306 review_document_part(parent = parent, part = part)
1307 elif review_after_display == 2: # review if no review by me exists
1308 review_by_me = filter(lambda rev: rev['is_your_review'], part.get_reviews())
1309 if len(review_by_me) == 0:
1310 review_document_part(parent = parent, part = part)
1311 elif review_after_display == 3:
1312 if len(part.get_reviews()) == 0:
1313 review_document_part(parent = parent, part = part)
1314 elif review_after_display == 4:
1315 reviewed_by_responsible = filter(lambda rev: rev['is_review_by_responsible_reviewer'], part.get_reviews())
1316 if len(reviewed_by_responsible) == 0:
1317 review_document_part(parent = parent, part = part)
1318
1319 return True
1320 #============================================================
1321 from Gnumed.wxGladeWidgets import wxgSelectablySortedDocTreePnl
1322
1323 -class cSelectablySortedDocTreePnl(wxgSelectablySortedDocTreePnl.wxgSelectablySortedDocTreePnl):
1324 """A panel with a document tree which can be sorted."""
1325 #--------------------------------------------------------
1326 # inherited event handlers
1327 #--------------------------------------------------------
1329 self._doc_tree.sort_mode = 'age'
1330 self._doc_tree.SetFocus()
1331 self._rbtn_sort_by_age.SetValue(True)
1332 #--------------------------------------------------------
1334 self._doc_tree.sort_mode = 'review'
1335 self._doc_tree.SetFocus()
1336 self._rbtn_sort_by_review.SetValue(True)
1337 #--------------------------------------------------------
1339 self._doc_tree.sort_mode = 'episode'
1340 self._doc_tree.SetFocus()
1341 self._rbtn_sort_by_episode.SetValue(True)
1342 #--------------------------------------------------------
1344 self._doc_tree.sort_mode = 'issue'
1345 self._doc_tree.SetFocus()
1346 self._rbtn_sort_by_issue.SetValue(True)
1347 #--------------------------------------------------------
1352 #============================================================
1354 # FIXME: handle expansion state
1355 """This wx.TreeCtrl derivative displays a tree view of stored medical documents.
1356
1357 It listens to document and patient changes and updated itself accordingly.
1358
1359 This acts on the current patient.
1360 """
1361 _sort_modes = ['age', 'review', 'episode', 'type', 'issue']
1362 _root_node_labels = None
1363 #--------------------------------------------------------
1365 """Set up our specialised tree.
1366 """
1367 kwds['style'] = wx.TR_NO_BUTTONS | wx.NO_BORDER | wx.TR_SINGLE
1368 wx.TreeCtrl.__init__(self, parent, id, *args, **kwds)
1369
1370 gmRegetMixin.cRegetOnPaintMixin.__init__(self)
1371
1372 tmp = _('available documents (%s)')
1373 unsigned = _('unsigned (%s) on top') % u'\u270D'
1374 cDocTree._root_node_labels = {
1375 'age': tmp % _('most recent on top'),
1376 'review': tmp % unsigned,
1377 'episode': tmp % _('sorted by episode'),
1378 'issue': tmp % _('sorted by health issue'),
1379 'type': tmp % _('sorted by type')
1380 }
1381
1382 self.root = None
1383 self.__sort_mode = 'age'
1384
1385 self.__build_context_menus()
1386 self.__register_interests()
1387 self._schedule_data_reget()
1388 #--------------------------------------------------------
1389 # external API
1390 #--------------------------------------------------------
1392
1393 node = self.GetSelection()
1394 node_data = self.GetPyData(node)
1395
1396 if not isinstance(node_data, gmDocuments.cDocumentPart):
1397 return True
1398
1399 self.__display_part(part = node_data)
1400 return True
1401 #--------------------------------------------------------
1402 # properties
1403 #--------------------------------------------------------
1406 #-----
1408 if mode is None:
1409 mode = 'age'
1410
1411 if mode == self.__sort_mode:
1412 return
1413
1414 if mode not in cDocTree._sort_modes:
1415 raise ValueError('invalid document tree sort mode [%s], valid modes: %s' % (mode, cDocTree._sort_modes))
1416
1417 self.__sort_mode = mode
1418
1419 curr_pat = gmPerson.gmCurrentPatient()
1420 if not curr_pat.connected:
1421 return
1422
1423 self._schedule_data_reget()
1424 #-----
1425 sort_mode = property(_get_sort_mode, _set_sort_mode)
1426 #--------------------------------------------------------
1427 # reget-on-paint API
1428 #--------------------------------------------------------
1430 curr_pat = gmPerson.gmCurrentPatient()
1431 if not curr_pat.connected:
1432 gmDispatcher.send(signal = 'statustext', msg = _('Cannot load documents. No active patient.'))
1433 return False
1434
1435 if not self.__populate_tree():
1436 return False
1437
1438 return True
1439 #--------------------------------------------------------
1440 # internal helpers
1441 #--------------------------------------------------------
1443 # connect handlers
1444 wx.EVT_TREE_ITEM_ACTIVATED (self, self.GetId(), self._on_activate)
1445 wx.EVT_TREE_ITEM_RIGHT_CLICK (self, self.GetId(), self.__on_right_click)
1446
1447 # wx.EVT_LEFT_DCLICK(self.tree, self.OnLeftDClick)
1448
1449 gmDispatcher.connect(signal = u'pre_patient_selection', receiver = self._on_pre_patient_selection)
1450 gmDispatcher.connect(signal = u'post_patient_selection', receiver = self._on_post_patient_selection)
1451 gmDispatcher.connect(signal = u'doc_mod_db', receiver = self._on_doc_mod_db)
1452 gmDispatcher.connect(signal = u'doc_page_mod_db', receiver = self._on_doc_page_mod_db)
1453 #--------------------------------------------------------
1531
1532 # document / description
1533 # self.__desc_menu = wx.Menu()
1534 # ID = wx.NewId()
1535 # self.__doc_context_menu.AppendMenu(ID, _('Descriptions ...'), self.__desc_menu)
1536
1537 # ID = wx.NewId()
1538 # self.__desc_menu.Append(ID, _('Add new description'))
1539 # wx.EVT_MENU(self.__desc_menu, ID, self.__add_doc_desc)
1540
1541 # ID = wx.NewId()
1542 # self.__desc_menu.Append(ID, _('Delete description'))
1543 # wx.EVT_MENU(self.__desc_menu, ID, self.__del_doc_desc)
1544
1545 # self.__desc_menu.AppendSeparator()
1546 #--------------------------------------------------------
1548
1549 wx.BeginBusyCursor()
1550
1551 # clean old tree
1552 if self.root is not None:
1553 self.DeleteAllItems()
1554
1555 # init new tree
1556 self.root = self.AddRoot(cDocTree._root_node_labels[self.__sort_mode], -1, -1)
1557 self.SetItemPyData(self.root, None)
1558 self.SetItemHasChildren(self.root, False)
1559
1560 # read documents from database
1561 curr_pat = gmPerson.gmCurrentPatient()
1562 docs_folder = curr_pat.get_document_folder()
1563 docs = docs_folder.get_documents()
1564
1565 if docs is None:
1566 gmGuiHelpers.gm_show_error (
1567 aMessage = _('Error searching documents.'),
1568 aTitle = _('loading document list')
1569 )
1570 # avoid recursion of GUI updating
1571 wx.EndBusyCursor()
1572 return True
1573
1574 if len(docs) == 0:
1575 wx.EndBusyCursor()
1576 return True
1577
1578 # fill new tree from document list
1579 self.SetItemHasChildren(self.root, True)
1580
1581 # add our documents as first level nodes
1582 intermediate_nodes = {}
1583 for doc in docs:
1584
1585 parts = doc.parts
1586
1587 label = _('%s%7s %s:%s (%s part(s)%s)') % (
1588 gmTools.bool2subst(doc.has_unreviewed_parts, gmTools.u_writing_hand, u'', u'?'),
1589 doc['clin_when'].strftime('%m/%Y'),
1590 doc['l10n_type'][:26],
1591 gmTools.coalesce(initial = doc['comment'], instead = u'', template_initial = u' %s'),
1592 len(parts),
1593 gmTools.coalesce(initial = doc['ext_ref'], instead = u'', template_initial = u', \u00BB%s\u00AB')
1594 )
1595
1596 # need intermediate branch level ?
1597 if self.__sort_mode == 'episode':
1598 lbl = u'%s%s' % (doc['episode'], gmTools.coalesce(doc['health_issue'], u'', u' (%s)'))
1599 if not intermediate_nodes.has_key(lbl):
1600 intermediate_nodes[lbl] = self.AppendItem(parent = self.root, text = lbl)
1601 self.SetItemBold(intermediate_nodes[lbl], bold = True)
1602 self.SetItemPyData(intermediate_nodes[lbl], None)
1603 self.SetItemHasChildren(intermediate_nodes[lbl], True)
1604 parent = intermediate_nodes[lbl]
1605 elif self.__sort_mode == 'type':
1606 lbl = doc['l10n_type']
1607 if not intermediate_nodes.has_key(lbl):
1608 intermediate_nodes[lbl] = self.AppendItem(parent = self.root, text = lbl)
1609 self.SetItemBold(intermediate_nodes[lbl], bold = True)
1610 self.SetItemPyData(intermediate_nodes[lbl], None)
1611 self.SetItemHasChildren(intermediate_nodes[lbl], True)
1612 parent = intermediate_nodes[lbl]
1613 elif self.__sort_mode == 'issue':
1614 if doc['health_issue'] is None:
1615 lbl = _('Unattributed episode: %s') % doc['episode']
1616 else:
1617 lbl = doc['health_issue']
1618 if not intermediate_nodes.has_key(lbl):
1619 intermediate_nodes[lbl] = self.AppendItem(parent = self.root, text = lbl)
1620 self.SetItemBold(intermediate_nodes[lbl], bold = True)
1621 self.SetItemPyData(intermediate_nodes[lbl], None)
1622 self.SetItemHasChildren(intermediate_nodes[lbl], True)
1623 parent = intermediate_nodes[lbl]
1624 else:
1625 parent = self.root
1626
1627 doc_node = self.AppendItem(parent = parent, text = label)
1628 #self.SetItemBold(doc_node, bold = True)
1629 self.SetItemPyData(doc_node, doc)
1630 if len(parts) == 0:
1631 self.SetItemHasChildren(doc_node, False)
1632 else:
1633 self.SetItemHasChildren(doc_node, True)
1634
1635 # now add parts as child nodes
1636 for part in parts:
1637 # if part['clinically_relevant']:
1638 # rel = ' [%s]' % _('Cave')
1639 # else:
1640 # rel = ''
1641 f_ext = u''
1642 if part['filename'] is not None:
1643 f_ext = os.path.splitext(part['filename'])[1].strip('.').strip()
1644 if f_ext != u'':
1645 f_ext = u' .' + f_ext.upper()
1646 label = '%s%s (%s%s)%s' % (
1647 gmTools.bool2str (
1648 boolean = part['reviewed'] or part['reviewed_by_you'] or part['reviewed_by_intended_reviewer'],
1649 true_str = u'',
1650 false_str = gmTools.u_writing_hand
1651 ),
1652 _('part %2s') % part['seq_idx'],
1653 gmTools.size2str(part['size']),
1654 f_ext,
1655 gmTools.coalesce (
1656 part['obj_comment'],
1657 u'',
1658 u': %s%%s%s' % (gmTools.u_left_double_angle_quote, gmTools.u_right_double_angle_quote)
1659 )
1660 )
1661
1662 part_node = self.AppendItem(parent = doc_node, text = label)
1663 self.SetItemPyData(part_node, part)
1664 self.SetItemHasChildren(part_node, False)
1665
1666 self.__sort_nodes()
1667 self.SelectItem(self.root)
1668
1669 # FIXME: apply expansion state if available or else ...
1670 # FIXME: ... uncollapse to default state
1671 self.Expand(self.root)
1672 if self.__sort_mode in ['episode', 'type', 'issue']:
1673 for key in intermediate_nodes.keys():
1674 self.Expand(intermediate_nodes[key])
1675
1676 wx.EndBusyCursor()
1677
1678 return True
1679 #------------------------------------------------------------------------
1681 """Used in sorting items.
1682
1683 -1: 1 < 2
1684 0: 1 = 2
1685 1: 1 > 2
1686 """
1687 # Windows can send bogus events so ignore that
1688 if not node1:
1689 _log.debug('invalid node 1')
1690 return 0
1691 if not node2:
1692 _log.debug('invalid node 2')
1693 return 0
1694 if not node1.IsOk():
1695 _log.debug('no data on node 1')
1696 return 0
1697 if not node2.IsOk():
1698 _log.debug('no data on node 2')
1699 return 0
1700
1701 data1 = self.GetPyData(node1)
1702 data2 = self.GetPyData(node2)
1703
1704 # doc node
1705 if isinstance(data1, gmDocuments.cDocument):
1706
1707 date_field = 'clin_when'
1708 #date_field = 'modified_when'
1709
1710 if self.__sort_mode == 'age':
1711 # reverse sort by date
1712 if data1[date_field] > data2[date_field]:
1713 return -1
1714 if data1[date_field] == data2[date_field]:
1715 return 0
1716 return 1
1717
1718 elif self.__sort_mode == 'episode':
1719 if data1['episode'] < data2['episode']:
1720 return -1
1721 if data1['episode'] == data2['episode']:
1722 # inner sort: reverse by date
1723 if data1[date_field] > data2[date_field]:
1724 return -1
1725 if data1[date_field] == data2[date_field]:
1726 return 0
1727 return 1
1728 return 1
1729
1730 elif self.__sort_mode == 'issue':
1731 if data1['health_issue'] < data2['health_issue']:
1732 return -1
1733 if data1['health_issue'] == data2['health_issue']:
1734 # inner sort: reverse by date
1735 if data1[date_field] > data2[date_field]:
1736 return -1
1737 if data1[date_field] == data2[date_field]:
1738 return 0
1739 return 1
1740 return 1
1741
1742 elif self.__sort_mode == 'review':
1743 # equality
1744 if data1.has_unreviewed_parts == data2.has_unreviewed_parts:
1745 # inner sort: reverse by date
1746 if data1[date_field] > data2[date_field]:
1747 return -1
1748 if data1[date_field] == data2[date_field]:
1749 return 0
1750 return 1
1751 if data1.has_unreviewed_parts:
1752 return -1
1753 return 1
1754
1755 elif self.__sort_mode == 'type':
1756 if data1['l10n_type'] < data2['l10n_type']:
1757 return -1
1758 if data1['l10n_type'] == data2['l10n_type']:
1759 # inner sort: reverse by date
1760 if data1[date_field] > data2[date_field]:
1761 return -1
1762 if data1[date_field] == data2[date_field]:
1763 return 0
1764 return 1
1765 return 1
1766
1767 else:
1768 _log.error('unknown document sort mode [%s], reverse-sorting by age', self.__sort_mode)
1769 # reverse sort by date
1770 if data1[date_field] > data2[date_field]:
1771 return -1
1772 if data1[date_field] == data2[date_field]:
1773 return 0
1774 return 1
1775
1776 # part node
1777 if isinstance(data1, gmDocuments.cDocumentPart):
1778 # compare sequence IDs (= "page" numbers)
1779 # FIXME: wrong order ?
1780 if data1['seq_idx'] < data2['seq_idx']:
1781 return -1
1782 if data1['seq_idx'] == data2['seq_idx']:
1783 return 0
1784 return 1
1785
1786 # else sort alphabetically
1787 if None in [data1, data2]:
1788 l1 = self.GetItemText(node1)
1789 l2 = self.GetItemText(node2)
1790 if l1 < l2:
1791 return -1
1792 if l1 == l2:
1793 return 0
1794 else:
1795 if data1 < data2:
1796 return -1
1797 if data1 == data2:
1798 return 0
1799 return 1
1800 #------------------------------------------------------------------------
1801 # event handlers
1802 #------------------------------------------------------------------------
1806 #------------------------------------------------------------------------
1810 #------------------------------------------------------------------------
1812 # FIXME: self.__store_expansion_history_in_db
1813
1814 # empty out tree
1815 if self.root is not None:
1816 self.DeleteAllItems()
1817 self.root = None
1818 #------------------------------------------------------------------------
1820 # FIXME: self.__load_expansion_history_from_db (but not apply it !)
1821 self._schedule_data_reget()
1822 #------------------------------------------------------------------------
1824 node = event.GetItem()
1825 node_data = self.GetPyData(node)
1826
1827 # exclude pseudo root node
1828 if node_data is None:
1829 return None
1830
1831 # expand/collapse documents on activation
1832 if isinstance(node_data, gmDocuments.cDocument):
1833 self.Toggle(node)
1834 return True
1835
1836 # string nodes are labels such as episodes which may or may not have children
1837 if type(node_data) == type('string'):
1838 self.Toggle(node)
1839 return True
1840
1841 self.__display_part(part = node_data)
1842 return True
1843 #--------------------------------------------------------
1845
1846 node = evt.GetItem()
1847 self.__curr_node_data = self.GetPyData(node)
1848
1849 # exclude pseudo root node
1850 if self.__curr_node_data is None:
1851 return None
1852
1853 # documents
1854 if isinstance(self.__curr_node_data, gmDocuments.cDocument):
1855 self.__handle_doc_context()
1856
1857 # parts
1858 if isinstance(self.__curr_node_data, gmDocuments.cDocumentPart):
1859 self.__handle_part_context()
1860
1861 del self.__curr_node_data
1862 evt.Skip()
1863 #--------------------------------------------------------
1865 self.__curr_node_data.set_as_active_photograph()
1866 #--------------------------------------------------------
1869 #--------------------------------------------------------
1872 #--------------------------------------------------------
1874 manage_document_descriptions(parent = self, document = self.__curr_node_data)
1875 #--------------------------------------------------------
1876 # internal API
1877 #--------------------------------------------------------
1879
1880 if start_node is None:
1881 start_node = self.GetRootItem()
1882
1883 # protect against empty tree where not even
1884 # a root node exists
1885 if not start_node.IsOk():
1886 return True
1887
1888 self.SortChildren(start_node)
1889
1890 child_node, cookie = self.GetFirstChild(start_node)
1891 while child_node.IsOk():
1892 self.__sort_nodes(start_node = child_node)
1893 child_node, cookie = self.GetNextChild(start_node, cookie)
1894
1895 return
1896 #--------------------------------------------------------
1899 #--------------------------------------------------------
1901
1902 # make active patient photograph
1903 if self.__curr_node_data['type'] == 'patient photograph':
1904 ID = wx.NewId()
1905 self.__part_context_menu.Append(ID, _('Activate as current photo'))
1906 wx.EVT_MENU(self.__part_context_menu, ID, self.__activate_as_current_photo)
1907 else:
1908 ID = None
1909
1910 self.PopupMenu(self.__part_context_menu, wx.DefaultPosition)
1911
1912 if ID is not None:
1913 self.__part_context_menu.Delete(ID)
1914 #--------------------------------------------------------
1915 # part level context menu handlers
1916 #--------------------------------------------------------
1918 """Display document part."""
1919
1920 # sanity check
1921 if part['size'] == 0:
1922 _log.debug('cannot display part [%s] - 0 bytes', part['pk_obj'])
1923 gmGuiHelpers.gm_show_error (
1924 aMessage = _('Document part does not seem to exist in database !'),
1925 aTitle = _('showing document')
1926 )
1927 return None
1928
1929 wx.BeginBusyCursor()
1930
1931 cfg = gmCfg.cCfgSQL()
1932
1933 # determine database export chunk size
1934 chunksize = int(
1935 cfg.get2 (
1936 option = "horstspace.blob_export_chunk_size",
1937 workplace = gmSurgery.gmCurrentPractice().active_workplace,
1938 bias = 'workplace',
1939 default = default_chunksize
1940 ))
1941
1942 # shall we force blocking during view ?
1943 block_during_view = bool( cfg.get2 (
1944 option = 'horstspace.document_viewer.block_during_view',
1945 workplace = gmSurgery.gmCurrentPractice().active_workplace,
1946 bias = 'user',
1947 default = None
1948 ))
1949
1950 # display it
1951 successful, msg = part.display_via_mime (
1952 # tmpdir = tmp_dir,
1953 chunksize = chunksize,
1954 block = block_during_view
1955 )
1956
1957 wx.EndBusyCursor()
1958
1959 if not successful:
1960 gmGuiHelpers.gm_show_error (
1961 aMessage = _('Cannot display document part:\n%s') % msg,
1962 aTitle = _('showing document')
1963 )
1964 return None
1965
1966 # handle review after display
1967 # 0: never
1968 # 1: always
1969 # 2: if no review by myself exists yet
1970 # 3: if no review at all exists yet
1971 # 4: if no review by responsible reviewer
1972 review_after_display = int(cfg.get2 (
1973 option = 'horstspace.document_viewer.review_after_display',
1974 workplace = gmSurgery.gmCurrentPractice().active_workplace,
1975 bias = 'user',
1976 default = 3
1977 ))
1978 if review_after_display == 1: # always review
1979 self.__review_part(part=part)
1980 elif review_after_display == 2: # review if no review by me exists
1981 review_by_me = filter(lambda rev: rev['is_your_review'], part.get_reviews())
1982 if len(review_by_me) == 0:
1983 self.__review_part(part = part)
1984 elif review_after_display == 3:
1985 if len(part.get_reviews()) == 0:
1986 self.__review_part(part = part)
1987 elif review_after_display == 4:
1988 reviewed_by_responsible = filter(lambda rev: rev['is_review_by_responsible_reviewer'], part.get_reviews())
1989 if len(reviewed_by_responsible) == 0:
1990 self.__review_part(part = part)
1991
1992 return True
1993 #--------------------------------------------------------
1995 dlg = cReviewDocPartDlg (
1996 parent = self,
1997 id = -1,
1998 part = part
1999 )
2000 dlg.ShowModal()
2001 dlg.Destroy()
2002 #--------------------------------------------------------
2004
2005 gmHooks.run_hook_script(hook = u'before_%s_doc_part' % action)
2006
2007 wx.BeginBusyCursor()
2008
2009 # detect wrapper
2010 found, external_cmd = gmShellAPI.detect_external_binary(u'gm-%s_doc' % action)
2011 if not found:
2012 found, external_cmd = gmShellAPI.detect_external_binary(u'gm-%s_doc.bat' % action)
2013 if not found:
2014 _log.error('neither of gm-%s_doc or gm-%s_doc.bat found', action, action)
2015 wx.EndBusyCursor()
2016 gmGuiHelpers.gm_show_error (
2017 _('Cannot %(l10n_action)s document part - %(l10n_action)s command not found.\n'
2018 '\n'
2019 'Either of gm_%(action)s_doc.sh or gm_%(action)s_doc.bat\n'
2020 'must be in the execution path. The command will\n'
2021 'be passed the filename to %(l10n_action)s.'
2022 ) % {'action': action, 'l10n_action': l10n_action},
2023 _('Processing document part: %s') % l10n_action
2024 )
2025 return
2026
2027 cfg = gmCfg.cCfgSQL()
2028
2029 # determine database export chunk size
2030 chunksize = int(cfg.get2 (
2031 option = "horstspace.blob_export_chunk_size",
2032 workplace = gmSurgery.gmCurrentPractice().active_workplace,
2033 bias = 'workplace',
2034 default = default_chunksize
2035 ))
2036
2037 part_file = self.__curr_node_data.export_to_file (
2038 # aTempDir = tmp_dir,
2039 aChunkSize = chunksize
2040 )
2041
2042 cmd = u'%s %s' % (external_cmd, part_file)
2043 success = gmShellAPI.run_command_in_shell (
2044 command = cmd,
2045 blocking = False
2046 )
2047
2048 wx.EndBusyCursor()
2049
2050 if not success:
2051 _log.error('%s command failed: [%s]', action, cmd)
2052 gmGuiHelpers.gm_show_error (
2053 _('Cannot %(l10n_action)s document part - %(l10n_action)s command failed.\n'
2054 '\n'
2055 'You may need to check and fix either of\n'
2056 ' gm_%(action)s_doc.sh (Unix/Mac) or\n'
2057 ' gm_%(action)s_doc.bat (Windows)\n'
2058 '\n'
2059 'The command is passed the filename to %(l10n_action)s.'
2060 ) % {'action': action, 'l10n_action': l10n_action},
2061 _('Processing document part: %s') % l10n_action
2062 )
2063 #--------------------------------------------------------
2064 # FIXME: icons in the plugin toolbar
2066 self.__process_part(action = u'print', l10n_action = _('print'))
2067 #--------------------------------------------------------
2069 self.__process_part(action = u'fax', l10n_action = _('fax'))
2070 #--------------------------------------------------------
2072 self.__process_part(action = u'mail', l10n_action = _('mail'))
2073 #--------------------------------------------------------
2074 # document level context menu handlers
2075 #--------------------------------------------------------
2077 enc = gmEMRStructWidgets.select_encounters (
2078 parent = self,
2079 patient = gmPerson.gmCurrentPatient()
2080 )
2081 if not enc:
2082 return
2083 self.__curr_node_data['pk_encounter'] = enc['pk_encounter']
2084 self.__curr_node_data.save()
2085 #--------------------------------------------------------
2087 enc = gmEMRStructItems.cEncounter(aPK_obj = self.__curr_node_data['pk_encounter'])
2088 gmEMRStructWidgets.edit_encounter(parent = self, encounter = enc)
2089 #--------------------------------------------------------
2091
2092 gmHooks.run_hook_script(hook = u'before_%s_doc' % action)
2093
2094 wx.BeginBusyCursor()
2095
2096 # detect wrapper
2097 found, external_cmd = gmShellAPI.detect_external_binary(u'gm-%s_doc' % action)
2098 if not found:
2099 found, external_cmd = gmShellAPI.detect_external_binary(u'gm-%s_doc.bat' % action)
2100 if not found:
2101 _log.error('neither of gm-%s_doc or gm-%s_doc.bat found', action, action)
2102 wx.EndBusyCursor()
2103 gmGuiHelpers.gm_show_error (
2104 _('Cannot %(l10n_action)s document - %(l10n_action)s command not found.\n'
2105 '\n'
2106 'Either of gm_%(action)s_doc.sh or gm_%(action)s_doc.bat\n'
2107 'must be in the execution path. The command will\n'
2108 'be passed a list of filenames to %(l10n_action)s.'
2109 ) % {'action': action, 'l10n_action': l10n_action},
2110 _('Processing document: %s') % l10n_action
2111 )
2112 return
2113
2114 cfg = gmCfg.cCfgSQL()
2115
2116 # determine database export chunk size
2117 chunksize = int(cfg.get2 (
2118 option = "horstspace.blob_export_chunk_size",
2119 workplace = gmSurgery.gmCurrentPractice().active_workplace,
2120 bias = 'workplace',
2121 default = default_chunksize
2122 ))
2123
2124 part_files = self.__curr_node_data.export_parts_to_files(chunksize = chunksize)
2125
2126 cmd = external_cmd + u' ' + u' '.join(part_files)
2127 success = gmShellAPI.run_command_in_shell (
2128 command = cmd,
2129 blocking = False
2130 )
2131
2132 wx.EndBusyCursor()
2133
2134 if not success:
2135 _log.error('%s command failed: [%s]', action, cmd)
2136 gmGuiHelpers.gm_show_error (
2137 _('Cannot %(l10n_action)s document - %(l10n_action)s command failed.\n'
2138 '\n'
2139 'You may need to check and fix either of\n'
2140 ' gm_%(action)s_doc.sh (Unix/Mac) or\n'
2141 ' gm_%(action)s_doc.bat (Windows)\n'
2142 '\n'
2143 'The command is passed a list of filenames to %(l10n_action)s.'
2144 ) % {'action': action, 'l10n_action': l10n_action},
2145 _('Processing document: %s') % l10n_action
2146 )
2147 #--------------------------------------------------------
2148 # FIXME: icons in the plugin toolbar
2150 self.__process_doc(action = u'print', l10n_action = _('print'))
2151 #--------------------------------------------------------
2153 self.__process_doc(action = u'fax', l10n_action = _('fax'))
2154 #--------------------------------------------------------
2156 self.__process_doc(action = u'mail', l10n_action = _('mail'))
2157 #--------------------------------------------------------
2159
2160 gmHooks.run_hook_script(hook = u'before_external_doc_access')
2161
2162 wx.BeginBusyCursor()
2163
2164 # detect wrapper
2165 found, external_cmd = gmShellAPI.detect_external_binary(u'gm_access_external_doc.sh')
2166 if not found:
2167 found, external_cmd = gmShellAPI.detect_external_binary(u'gm_access_external_doc.bat')
2168 if not found:
2169 _log.error('neither of gm_access_external_doc.sh or .bat found')
2170 wx.EndBusyCursor()
2171 gmGuiHelpers.gm_show_error (
2172 _('Cannot access external document - access command not found.\n'
2173 '\n'
2174 'Either of gm_access_external_doc.sh or *.bat must be\n'
2175 'in the execution path. The command will be passed the\n'
2176 'document type and the reference URL for processing.'
2177 ),
2178 _('Accessing external document')
2179 )
2180 return
2181
2182 cmd = u'%s "%s" "%s"' % (external_cmd, self.__curr_node_data['type'], self.__curr_node_data['ext_ref'])
2183 success = gmShellAPI.run_command_in_shell (
2184 command = cmd,
2185 blocking = False
2186 )
2187
2188 wx.EndBusyCursor()
2189
2190 if not success:
2191 _log.error('External access command failed: [%s]', cmd)
2192 gmGuiHelpers.gm_show_error (
2193 _('Cannot access external document - access command failed.\n'
2194 '\n'
2195 'You may need to check and fix either of\n'
2196 ' gm_access_external_doc.sh (Unix/Mac) or\n'
2197 ' gm_access_external_doc.bat (Windows)\n'
2198 '\n'
2199 'The command is passed the document type and the\n'
2200 'external reference URL on the command line.'
2201 ),
2202 _('Accessing external document')
2203 )
2204 #--------------------------------------------------------
2206 """Export document into directory.
2207
2208 - one file per object
2209 - into subdirectory named after patient
2210 """
2211 pat = gmPerson.gmCurrentPatient()
2212 dname = '%s-%s%s' % (
2213 self.__curr_node_data['l10n_type'],
2214 self.__curr_node_data['clin_when'].strftime('%Y-%m-%d'),
2215 gmTools.coalesce(self.__curr_node_data['ext_ref'], '', '-%s').replace(' ', '_')
2216 )
2217 def_dir = os.path.expanduser(os.path.join('~', 'gnumed', 'export', 'docs', pat['dirname'], dname))
2218 gmTools.mkdir(def_dir)
2219
2220 dlg = wx.DirDialog (
2221 parent = self,
2222 message = _('Save document into directory ...'),
2223 defaultPath = def_dir,
2224 style = wx.DD_DEFAULT_STYLE
2225 )
2226 result = dlg.ShowModal()
2227 dirname = dlg.GetPath()
2228 dlg.Destroy()
2229
2230 if result != wx.ID_OK:
2231 return True
2232
2233 wx.BeginBusyCursor()
2234
2235 cfg = gmCfg.cCfgSQL()
2236
2237 # determine database export chunk size
2238 chunksize = int(cfg.get2 (
2239 option = "horstspace.blob_export_chunk_size",
2240 workplace = gmSurgery.gmCurrentPractice().active_workplace,
2241 bias = 'workplace',
2242 default = default_chunksize
2243 ))
2244
2245 fnames = self.__curr_node_data.export_parts_to_files(export_dir = dirname, chunksize = chunksize)
2246
2247 wx.EndBusyCursor()
2248
2249 gmDispatcher.send(signal='statustext', msg=_('Successfully exported %s parts into the directory [%s].') % (len(fnames), dirname))
2250
2251 return True
2252 #--------------------------------------------------------
2254 result = gmGuiHelpers.gm_show_question (
2255 aMessage = _('Are you sure you want to delete the document ?'),
2256 aTitle = _('Deleting document')
2257 )
2258 if result is True:
2259 curr_pat = gmPerson.gmCurrentPatient()
2260 emr = curr_pat.get_emr()
2261 enc = emr.active_encounter
2262 gmDocuments.delete_document(document_id = self.__curr_node_data['pk_doc'], encounter_id = enc['pk_encounter'])
2263 #============================================================
2264 # main
2265 #------------------------------------------------------------
2266 if __name__ == '__main__':
2267
2268 gmI18N.activate_locale()
2269 gmI18N.install_domain(domain = 'gnumed')
2270
2271 #----------------------------------------
2272 #----------------------------------------
2273 if (len(sys.argv) > 1) and (sys.argv[1] == 'test'):
2274 # test_*()
2275 pass
2276
2277 #============================================================
2278
| Home | Trees | Indices | Help |
|
|---|
| Generated by Epydoc 3.0.1 on Mon Jan 23 03:58:59 2012 | http://epydoc.sourceforge.net |