| 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 = gmDocumentWidgets.cEditDocumentTypesDlg(parent = self, id=-1)
251 dlg = cEditDocumentTypesDlg(parent = parent)
252 dlg.ShowModal()
253 #============================================================
254 from Gnumed.wxGladeWidgets import wxgEditDocumentTypesDlg
255
261
262 #============================================================
263 from Gnumed.wxGladeWidgets import wxgEditDocumentTypesPnl
264
266 """A panel grouping together fields to edit the list of document types."""
267
269 wxgEditDocumentTypesPnl.wxgEditDocumentTypesPnl.__init__(self, *args, **kwargs)
270 self.__init_ui()
271 self.__register_interests()
272 self.repopulate_ui()
273 #--------------------------------------------------------
275 self._LCTRL_doc_type.set_columns([_('Type'), _('Translation'), _('User defined'), _('In use')])
276 self._LCTRL_doc_type.set_column_widths()
277 #--------------------------------------------------------
280 #--------------------------------------------------------
282 wx.CallAfter(self.repopulate_ui)
283 #--------------------------------------------------------
285
286 self._LCTRL_doc_type.DeleteAllItems()
287
288 doc_types = gmDocuments.get_document_types()
289 pos = len(doc_types) + 1
290
291 for doc_type in doc_types:
292 row_num = self._LCTRL_doc_type.InsertStringItem(pos, label = doc_type['type'])
293 self._LCTRL_doc_type.SetStringItem(index = row_num, col = 1, label = doc_type['l10n_type'])
294 if doc_type['is_user_defined']:
295 self._LCTRL_doc_type.SetStringItem(index = row_num, col = 2, label = ' X ')
296 if doc_type['is_in_use']:
297 self._LCTRL_doc_type.SetStringItem(index = row_num, col = 3, label = ' X ')
298
299 if len(doc_types) > 0:
300 self._LCTRL_doc_type.set_data(data = doc_types)
301 self._LCTRL_doc_type.SetColumnWidth(col=0, width=wx.LIST_AUTOSIZE)
302 self._LCTRL_doc_type.SetColumnWidth(col=1, width=wx.LIST_AUTOSIZE)
303 self._LCTRL_doc_type.SetColumnWidth(col=2, width=wx.LIST_AUTOSIZE_USEHEADER)
304 self._LCTRL_doc_type.SetColumnWidth(col=3, width=wx.LIST_AUTOSIZE_USEHEADER)
305
306 self._TCTRL_type.SetValue('')
307 self._TCTRL_l10n_type.SetValue('')
308
309 self._BTN_set_translation.Enable(False)
310 self._BTN_delete.Enable(False)
311 self._BTN_add.Enable(False)
312 self._BTN_reassign.Enable(False)
313
314 self._LCTRL_doc_type.SetFocus()
315 #--------------------------------------------------------
316 # event handlers
317 #--------------------------------------------------------
319 doc_type = self._LCTRL_doc_type.get_selected_item_data()
320
321 self._TCTRL_type.SetValue(doc_type['type'])
322 self._TCTRL_l10n_type.SetValue(doc_type['l10n_type'])
323
324 self._BTN_set_translation.Enable(True)
325 self._BTN_delete.Enable(not bool(doc_type['is_in_use']))
326 self._BTN_add.Enable(False)
327 self._BTN_reassign.Enable(True)
328
329 return
330 #--------------------------------------------------------
332 self._BTN_set_translation.Enable(False)
333 self._BTN_delete.Enable(False)
334 self._BTN_reassign.Enable(False)
335
336 self._BTN_add.Enable(True)
337 # self._LCTRL_doc_type.deselect_selected_item()
338 return
339 #--------------------------------------------------------
346 #--------------------------------------------------------
363 #--------------------------------------------------------
373 #--------------------------------------------------------
405 #============================================================
407 """Let user select a document type."""
409
410 gmPhraseWheel.cPhraseWheel.__init__(self, *args, **kwargs)
411
412 mp = gmMatchProvider.cMatchProvider_SQL2 (
413 queries = [
414 u"""SELECT
415 data,
416 field_label,
417 list_label
418 FROM ((
419 SELECT
420 pk_doc_type AS data,
421 l10n_type AS field_label,
422 l10n_type AS list_label,
423 1 AS rank
424 FROM blobs.v_doc_type
425 WHERE
426 is_user_defined IS True
427 AND
428 l10n_type %(fragment_condition)s
429 ) UNION (
430 SELECT
431 pk_doc_type AS data,
432 l10n_type AS field_label,
433 l10n_type AS list_label,
434 2 AS rank
435 FROM blobs.v_doc_type
436 WHERE
437 is_user_defined IS False
438 AND
439 l10n_type %(fragment_condition)s
440 )) AS q1
441 ORDER BY q1.rank, q1.list_label"""]
442 )
443 mp.setThresholds(2, 4, 6)
444
445 self.matcher = mp
446 self.picklist_delay = 50
447
448 self.SetToolTipString(_('Select the document type.'))
449 #--------------------------------------------------------
451
452 doc_type = self.GetValue().strip()
453 if doc_type == u'':
454 gmDispatcher.send(signal = u'statustext', msg = _('Cannot create document type without name.'), beep = True)
455 _log.debug('cannot create document type without name')
456 return
457
458 pk = gmDocuments.create_document_type(doc_type)['pk_doc_type']
459 if pk is None:
460 self.data = {}
461 else:
462 self.SetText (
463 value = doc_type,
464 data = pk
465 )
466 #============================================================
467 from Gnumed.wxGladeWidgets import wxgReviewDocPartDlg
468
471 """Support parts and docs now.
472 """
473 part = kwds['part']
474 del kwds['part']
475 wxgReviewDocPartDlg.wxgReviewDocPartDlg.__init__(self, *args, **kwds)
476
477 if isinstance(part, gmDocuments.cDocumentPart):
478 self.__part = part
479 self.__doc = self.__part.get_containing_document()
480 self.__reviewing_doc = False
481 elif isinstance(part, gmDocuments.cDocument):
482 self.__doc = part
483 if len(self.__doc.parts) == 0:
484 self.__part = None
485 else:
486 self.__part = self.__doc.parts[0]
487 self.__reviewing_doc = True
488 else:
489 raise ValueError('<part> must be gmDocuments.cDocument or gmDocuments.cDocumentPart instance, got <%s>' % type(part))
490
491 self.__init_ui_data()
492 #--------------------------------------------------------
493 # internal API
494 #--------------------------------------------------------
496 # FIXME: fix this
497 # associated episode (add " " to avoid popping up pick list)
498 self._PhWheel_episode.SetText('%s ' % self.__doc['episode'], self.__doc['pk_episode'])
499 self._PhWheel_doc_type.SetText(value = self.__doc['l10n_type'], data = self.__doc['pk_type'])
500 self._PhWheel_doc_type.add_callback_on_set_focus(self._on_doc_type_gets_focus)
501 self._PhWheel_doc_type.add_callback_on_lose_focus(self._on_doc_type_loses_focus)
502
503 if self.__reviewing_doc:
504 self._PRW_doc_comment.SetText(gmTools.coalesce(self.__doc['comment'], ''))
505 self._PRW_doc_comment.set_context(context = 'pk_doc_type', val = self.__doc['pk_type'])
506 else:
507 self._PRW_doc_comment.SetText(gmTools.coalesce(self.__part['obj_comment'], ''))
508
509 fts = gmDateTime.cFuzzyTimestamp(timestamp = self.__doc['clin_when'])
510 self._PhWheel_doc_date.SetText(fts.strftime('%Y-%m-%d'), fts)
511 self._TCTRL_reference.SetValue(gmTools.coalesce(self.__doc['ext_ref'], ''))
512 if self.__reviewing_doc:
513 self._TCTRL_filename.Enable(False)
514 self._SPINCTRL_seq_idx.Enable(False)
515 else:
516 self._TCTRL_filename.SetValue(gmTools.coalesce(self.__part['filename'], ''))
517 self._SPINCTRL_seq_idx.SetValue(gmTools.coalesce(self.__part['seq_idx'], 0))
518
519 self._LCTRL_existing_reviews.InsertColumn(0, _('who'))
520 self._LCTRL_existing_reviews.InsertColumn(1, _('when'))
521 self._LCTRL_existing_reviews.InsertColumn(2, _('+/-'))
522 self._LCTRL_existing_reviews.InsertColumn(3, _('!'))
523 self._LCTRL_existing_reviews.InsertColumn(4, _('comment'))
524
525 self.__reload_existing_reviews()
526
527 if self._LCTRL_existing_reviews.GetItemCount() > 0:
528 self._LCTRL_existing_reviews.SetColumnWidth(col=0, width=wx.LIST_AUTOSIZE)
529 self._LCTRL_existing_reviews.SetColumnWidth(col=1, width=wx.LIST_AUTOSIZE)
530 self._LCTRL_existing_reviews.SetColumnWidth(col=2, width=wx.LIST_AUTOSIZE_USEHEADER)
531 self._LCTRL_existing_reviews.SetColumnWidth(col=3, width=wx.LIST_AUTOSIZE_USEHEADER)
532 self._LCTRL_existing_reviews.SetColumnWidth(col=4, width=wx.LIST_AUTOSIZE)
533
534 if self.__part is None:
535 self._ChBOX_review.SetValue(False)
536 self._ChBOX_review.Enable(False)
537 self._ChBOX_abnormal.Enable(False)
538 self._ChBOX_relevant.Enable(False)
539 self._ChBOX_sign_all_pages.Enable(False)
540 else:
541 me = gmStaff.gmCurrentProvider()
542 if self.__part['pk_intended_reviewer'] == me['pk_staff']:
543 msg = _('(you are the primary reviewer)')
544 else:
545 msg = _('(someone else is the primary reviewer)')
546 self._TCTRL_responsible.SetValue(msg)
547 # init my review if any
548 if self.__part['reviewed_by_you']:
549 revs = self.__part.get_reviews()
550 for rev in revs:
551 if rev['is_your_review']:
552 self._ChBOX_abnormal.SetValue(bool(rev[2]))
553 self._ChBOX_relevant.SetValue(bool(rev[3]))
554 break
555
556 self._ChBOX_sign_all_pages.SetValue(self.__reviewing_doc)
557
558 return True
559 #--------------------------------------------------------
561 self._LCTRL_existing_reviews.DeleteAllItems()
562 if self.__part is None:
563 return True
564 revs = self.__part.get_reviews() # FIXME: this is ugly as sin, it should be dicts, not lists
565 if len(revs) == 0:
566 return True
567 # find special reviews
568 review_by_responsible_doc = None
569 reviews_by_others = []
570 for rev in revs:
571 if rev['is_review_by_responsible_reviewer'] and not rev['is_your_review']:
572 review_by_responsible_doc = rev
573 if not (rev['is_review_by_responsible_reviewer'] or rev['is_your_review']):
574 reviews_by_others.append(rev)
575 # display them
576 if review_by_responsible_doc is not None:
577 row_num = self._LCTRL_existing_reviews.InsertStringItem(sys.maxint, label=review_by_responsible_doc[0])
578 self._LCTRL_existing_reviews.SetItemTextColour(row_num, col=wx.BLUE)
579 self._LCTRL_existing_reviews.SetStringItem(index = row_num, col=0, label=review_by_responsible_doc[0])
580 self._LCTRL_existing_reviews.SetStringItem(index = row_num, col=1, label=review_by_responsible_doc[1].strftime('%x %H:%M'))
581 if review_by_responsible_doc['is_technically_abnormal']:
582 self._LCTRL_existing_reviews.SetStringItem(index = row_num, col=2, label=u'X')
583 if review_by_responsible_doc['clinically_relevant']:
584 self._LCTRL_existing_reviews.SetStringItem(index = row_num, col=3, label=u'X')
585 self._LCTRL_existing_reviews.SetStringItem(index = row_num, col=4, label=review_by_responsible_doc[6])
586 row_num += 1
587 for rev in reviews_by_others:
588 row_num = self._LCTRL_existing_reviews.InsertStringItem(sys.maxint, label=rev[0])
589 self._LCTRL_existing_reviews.SetStringItem(index = row_num, col=0, label=rev[0])
590 self._LCTRL_existing_reviews.SetStringItem(index = row_num, col=1, label=rev[1].strftime('%x %H:%M'))
591 if rev['is_technically_abnormal']:
592 self._LCTRL_existing_reviews.SetStringItem(index = row_num, col=2, label=u'X')
593 if rev['clinically_relevant']:
594 self._LCTRL_existing_reviews.SetStringItem(index = row_num, col=3, label=u'X')
595 self._LCTRL_existing_reviews.SetStringItem(index = row_num, col=4, label=rev[6])
596 return True
597 #--------------------------------------------------------
598 # event handlers
599 #--------------------------------------------------------
687 #--------------------------------------------------------
689 state = self._ChBOX_review.GetValue()
690 self._ChBOX_abnormal.Enable(enable = state)
691 self._ChBOX_relevant.Enable(enable = state)
692 self._ChBOX_responsible.Enable(enable = state)
693 #--------------------------------------------------------
695 """Per Jim: Changing the doc type happens a lot more often
696 then correcting spelling, hence select-all on getting focus.
697 """
698 self._PhWheel_doc_type.SetSelection(-1, -1)
699 #--------------------------------------------------------
701 pk_doc_type = self._PhWheel_doc_type.GetData()
702 if pk_doc_type is None:
703 self._PRW_doc_comment.unset_context(context = 'pk_doc_type')
704 else:
705 self._PRW_doc_comment.set_context(context = 'pk_doc_type', val = pk_doc_type)
706 return True
707 #============================================================
709
710 _log.debug('acquiring images from [%s]', device)
711
712 # do not import globally since we might want to use
713 # this module without requiring any scanner to be available
714 from Gnumed.pycommon import gmScanBackend
715 try:
716 fnames = gmScanBackend.acquire_pages_into_files (
717 device = device,
718 delay = 5,
719 calling_window = calling_window
720 )
721 except OSError:
722 _log.exception('problem acquiring image from source')
723 gmGuiHelpers.gm_show_error (
724 aMessage = _(
725 'No images could be acquired from the source.\n\n'
726 'This may mean the scanner driver is not properly installed.\n\n'
727 'On Windows you must install the TWAIN Python module\n'
728 'while on Linux and MacOSX it is recommended to install\n'
729 'the XSane package.'
730 ),
731 aTitle = _('Acquiring images')
732 )
733 return None
734
735 _log.debug('acquired %s images', len(fnames))
736
737 return fnames
738 #------------------------------------------------------------
739 from Gnumed.wxGladeWidgets import wxgScanIdxPnl
740
743 wxgScanIdxPnl.wxgScanIdxPnl.__init__(self, *args, **kwds)
744 gmPlugin.cPatientChange_PluginMixin.__init__(self)
745
746 self._PhWheel_reviewer.matcher = gmPerson.cMatchProvider_Provider()
747
748 self.__init_ui_data()
749 self._PhWheel_doc_type.add_callback_on_lose_focus(self._on_doc_type_loses_focus)
750
751 # make me and listctrl a file drop target
752 dt = gmGuiHelpers.cFileDropTarget(self)
753 self.SetDropTarget(dt)
754 dt = gmGuiHelpers.cFileDropTarget(self._LBOX_doc_pages)
755 self._LBOX_doc_pages.SetDropTarget(dt)
756 self._LBOX_doc_pages.add_filenames = self.add_filenames_to_listbox
757
758 # do not import globally since we might want to use
759 # this module without requiring any scanner to be available
760 from Gnumed.pycommon import gmScanBackend
761 self.scan_module = gmScanBackend
762 #--------------------------------------------------------
763 # file drop target API
764 #--------------------------------------------------------
766 self.add_filenames(filenames=filenames)
767 #--------------------------------------------------------
769 pat = gmPerson.gmCurrentPatient()
770 if not pat.connected:
771 gmDispatcher.send(signal='statustext', msg=_('Cannot accept new documents. No active patient.'))
772 return
773
774 # dive into folders dropped onto us and extract files (one level deep only)
775 real_filenames = []
776 for pathname in filenames:
777 try:
778 files = os.listdir(pathname)
779 gmDispatcher.send(signal='statustext', msg=_('Extracting files from folder [%s] ...') % pathname)
780 for file in files:
781 fullname = os.path.join(pathname, file)
782 if not os.path.isfile(fullname):
783 continue
784 real_filenames.append(fullname)
785 except OSError:
786 real_filenames.append(pathname)
787
788 self.acquired_pages.extend(real_filenames)
789 self.__reload_LBOX_doc_pages()
790 #--------------------------------------------------------
793 #--------------------------------------------------------
794 # patient change plugin API
795 #--------------------------------------------------------
799 #--------------------------------------------------------
802 #--------------------------------------------------------
803 # internal API
804 #--------------------------------------------------------
806 # -----------------------------
807 self._PhWheel_episode.SetText('')
808 self._PhWheel_doc_type.SetText('')
809 # -----------------------------
810 # FIXME: make this configurable: either now() or last_date()
811 fts = gmDateTime.cFuzzyTimestamp()
812 self._PhWheel_doc_date.SetText(fts.strftime('%Y-%m-%d'), fts)
813 self._PRW_doc_comment.SetText('')
814 # FIXME: should be set to patient's primary doc
815 self._PhWheel_reviewer.selection_only = True
816 me = gmStaff.gmCurrentProvider()
817 self._PhWheel_reviewer.SetText (
818 value = u'%s (%s%s %s)' % (me['short_alias'], me['title'], me['firstnames'], me['lastnames']),
819 data = me['pk_staff']
820 )
821 # -----------------------------
822 # FIXME: set from config item
823 self._ChBOX_reviewed.SetValue(False)
824 self._ChBOX_abnormal.Disable()
825 self._ChBOX_abnormal.SetValue(False)
826 self._ChBOX_relevant.Disable()
827 self._ChBOX_relevant.SetValue(False)
828 # -----------------------------
829 self._TBOX_description.SetValue('')
830 # -----------------------------
831 # the list holding our page files
832 self._LBOX_doc_pages.Clear()
833 self.acquired_pages = []
834 #--------------------------------------------------------
836 self._LBOX_doc_pages.Clear()
837 if len(self.acquired_pages) > 0:
838 for i in range(len(self.acquired_pages)):
839 fname = self.acquired_pages[i]
840 self._LBOX_doc_pages.Append(_('part %s: %s') % (i+1, fname), fname)
841 #--------------------------------------------------------
843 title = _('saving document')
844
845 if self.acquired_pages is None or len(self.acquired_pages) == 0:
846 dbcfg = gmCfg.cCfgSQL()
847 allow_empty = bool(dbcfg.get2 (
848 option = u'horstspace.scan_index.allow_partless_documents',
849 workplace = gmSurgery.gmCurrentPractice().active_workplace,
850 bias = 'user',
851 default = False
852 ))
853 if allow_empty:
854 save_empty = gmGuiHelpers.gm_show_question (
855 aMessage = _('No parts to save. Really save an empty document as a reference ?'),
856 aTitle = title
857 )
858 if not save_empty:
859 return False
860 else:
861 gmGuiHelpers.gm_show_error (
862 aMessage = _('No parts to save. Aquire some parts first.'),
863 aTitle = title
864 )
865 return False
866
867 doc_type_pk = self._PhWheel_doc_type.GetData(can_create = True)
868 if doc_type_pk is None:
869 gmGuiHelpers.gm_show_error (
870 aMessage = _('No document type applied. Choose a document type'),
871 aTitle = title
872 )
873 return False
874
875 # this should be optional, actually
876 # if self._PRW_doc_comment.GetValue().strip() == '':
877 # gmGuiHelpers.gm_show_error (
878 # aMessage = _('No document comment supplied. Add a comment for this document.'),
879 # aTitle = title
880 # )
881 # return False
882
883 if self._PhWheel_episode.GetValue().strip() == '':
884 gmGuiHelpers.gm_show_error (
885 aMessage = _('You must select an episode to save this document under.'),
886 aTitle = title
887 )
888 return False
889
890 if self._PhWheel_reviewer.GetData() is None:
891 gmGuiHelpers.gm_show_error (
892 aMessage = _('You need to select from the list of staff members the doctor who is intended to sign the document.'),
893 aTitle = title
894 )
895 return False
896
897 return True
898 #--------------------------------------------------------
900
901 if not reconfigure:
902 dbcfg = gmCfg.cCfgSQL()
903 device = dbcfg.get2 (
904 option = 'external.xsane.default_device',
905 workplace = gmSurgery.gmCurrentPractice().active_workplace,
906 bias = 'workplace',
907 default = ''
908 )
909 if device.strip() == u'':
910 device = None
911 if device is not None:
912 return device
913
914 try:
915 devices = self.scan_module.get_devices()
916 except:
917 _log.exception('cannot retrieve list of image sources')
918 gmDispatcher.send(signal = 'statustext', msg = _('There is no scanner support installed on this machine.'))
919 return None
920
921 if devices is None:
922 # get_devices() not implemented for TWAIN yet
923 # XSane has its own chooser (so does TWAIN)
924 return None
925
926 if len(devices) == 0:
927 gmDispatcher.send(signal = 'statustext', msg = _('Cannot find an active scanner.'))
928 return None
929
930 # device_names = []
931 # for device in devices:
932 # device_names.append('%s (%s)' % (device[2], device[0]))
933
934 device = gmListWidgets.get_choices_from_list (
935 parent = self,
936 msg = _('Select an image capture device'),
937 caption = _('device selection'),
938 choices = [ '%s (%s)' % (d[2], d[0]) for d in devices ],
939 columns = [_('Device')],
940 data = devices,
941 single_selection = True
942 )
943 if device is None:
944 return None
945
946 # FIXME: add support for actually reconfiguring
947 return device[0]
948 #--------------------------------------------------------
949 # event handling API
950 #--------------------------------------------------------
952
953 chosen_device = self.get_device_to_use()
954
955 tmpdir = os.path.expanduser(os.path.join('~', '.gnumed', 'tmp'))
956 try:
957 gmTools.mkdir(tmpdir)
958 except:
959 tmpdir = None
960
961 # FIXME: configure whether to use XSane or sane directly
962 # FIXME: add support for xsane_device_settings argument
963 try:
964 fnames = self.scan_module.acquire_pages_into_files (
965 device = chosen_device,
966 delay = 5,
967 tmpdir = tmpdir,
968 calling_window = self
969 )
970 except OSError:
971 _log.exception('problem acquiring image from source')
972 gmGuiHelpers.gm_show_error (
973 aMessage = _(
974 'No pages could be acquired from the source.\n\n'
975 'This may mean the scanner driver is not properly installed.\n\n'
976 'On Windows you must install the TWAIN Python module\n'
977 'while on Linux and MacOSX it is recommended to install\n'
978 'the XSane package.'
979 ),
980 aTitle = _('acquiring page')
981 )
982 return None
983
984 if len(fnames) == 0: # no pages scanned
985 return True
986
987 self.acquired_pages.extend(fnames)
988 self.__reload_LBOX_doc_pages()
989
990 return True
991 #--------------------------------------------------------
993 # patient file chooser
994 dlg = wx.FileDialog (
995 parent = None,
996 message = _('Choose a file'),
997 defaultDir = os.path.expanduser(os.path.join('~', 'gnumed')),
998 defaultFile = '',
999 wildcard = "%s (*)|*|TIFFs (*.tif)|*.tif|JPEGs (*.jpg)|*.jpg|%s (*.*)|*.*" % (_('all files'), _('all files (Win)')),
1000 style = wx.OPEN | wx.HIDE_READONLY | wx.FILE_MUST_EXIST | wx.MULTIPLE
1001 )
1002 result = dlg.ShowModal()
1003 if result != wx.ID_CANCEL:
1004 files = dlg.GetPaths()
1005 for file in files:
1006 self.acquired_pages.append(file)
1007 self.__reload_LBOX_doc_pages()
1008 dlg.Destroy()
1009 #--------------------------------------------------------
1011 # did user select a page ?
1012 page_idx = self._LBOX_doc_pages.GetSelection()
1013 if page_idx == -1:
1014 gmGuiHelpers.gm_show_info (
1015 aMessage = _('You must select a part before you can view it.'),
1016 aTitle = _('displaying part')
1017 )
1018 return None
1019 # now, which file was that again ?
1020 page_fname = self._LBOX_doc_pages.GetClientData(page_idx)
1021
1022 (result, msg) = gmMimeLib.call_viewer_on_file(page_fname)
1023 if not result:
1024 gmGuiHelpers.gm_show_warning (
1025 aMessage = _('Cannot display document part:\n%s') % msg,
1026 aTitle = _('displaying part')
1027 )
1028 return None
1029 return 1
1030 #--------------------------------------------------------
1032 page_idx = self._LBOX_doc_pages.GetSelection()
1033 if page_idx == -1:
1034 gmGuiHelpers.gm_show_info (
1035 aMessage = _('You must select a part before you can delete it.'),
1036 aTitle = _('deleting part')
1037 )
1038 return None
1039 page_fname = self._LBOX_doc_pages.GetClientData(page_idx)
1040
1041 # 1) del item from self.acquired_pages
1042 self.acquired_pages[page_idx:(page_idx+1)] = []
1043
1044 # 2) reload list box
1045 self.__reload_LBOX_doc_pages()
1046
1047 # 3) optionally kill file in the file system
1048 do_delete = gmGuiHelpers.gm_show_question (
1049 _('The part has successfully been removed from the document.\n'
1050 '\n'
1051 'Do you also want to permanently delete the file\n'
1052 '\n'
1053 ' [%s]\n'
1054 '\n'
1055 'from which this document part was loaded ?\n'
1056 '\n'
1057 'If it is a temporary file for a page you just scanned\n'
1058 'this makes a lot of sense. In other cases you may not\n'
1059 'want to lose the file.\n'
1060 '\n'
1061 'Pressing [YES] will permanently remove the file\n'
1062 'from your computer.\n'
1063 ) % page_fname,
1064 _('Removing document part')
1065 )
1066 if do_delete:
1067 try:
1068 os.remove(page_fname)
1069 except:
1070 _log.exception('Error deleting file.')
1071 gmGuiHelpers.gm_show_error (
1072 aMessage = _('Cannot delete part in file [%s].\n\nYou may not have write access to it.') % page_fname,
1073 aTitle = _('deleting part')
1074 )
1075
1076 return 1
1077 #--------------------------------------------------------
1079
1080 if not self.__valid_for_save():
1081 return False
1082
1083 wx.BeginBusyCursor()
1084
1085 pat = gmPerson.gmCurrentPatient()
1086 doc_folder = pat.get_document_folder()
1087 emr = pat.get_emr()
1088
1089 # create new document
1090 pk_episode = self._PhWheel_episode.GetData()
1091 if pk_episode is None:
1092 episode = emr.add_episode (
1093 episode_name = self._PhWheel_episode.GetValue().strip(),
1094 is_open = True
1095 )
1096 if episode is None:
1097 wx.EndBusyCursor()
1098 gmGuiHelpers.gm_show_error (
1099 aMessage = _('Cannot start episode [%s].') % self._PhWheel_episode.GetValue().strip(),
1100 aTitle = _('saving document')
1101 )
1102 return False
1103 pk_episode = episode['pk_episode']
1104
1105 encounter = emr.active_encounter['pk_encounter']
1106 document_type = self._PhWheel_doc_type.GetData()
1107 new_doc = doc_folder.add_document(document_type, encounter, pk_episode)
1108 if new_doc is None:
1109 wx.EndBusyCursor()
1110 gmGuiHelpers.gm_show_error (
1111 aMessage = _('Cannot create new document.'),
1112 aTitle = _('saving document')
1113 )
1114 return False
1115
1116 # update business object with metadata
1117 # - date of generation
1118 new_doc['clin_when'] = self._PhWheel_doc_date.GetData().get_pydt()
1119 # - external reference
1120 cfg = gmCfg.cCfgSQL()
1121 generate_uuid = bool (
1122 cfg.get2 (
1123 option = 'horstspace.scan_index.generate_doc_uuid',
1124 workplace = gmSurgery.gmCurrentPractice().active_workplace,
1125 bias = 'user',
1126 default = False
1127 )
1128 )
1129 ref = None
1130 if generate_uuid:
1131 ref = gmDocuments.get_ext_ref()
1132 if ref is not None:
1133 new_doc['ext_ref'] = ref
1134 # - comment
1135 comment = self._PRW_doc_comment.GetLineText(0).strip()
1136 if comment != u'':
1137 new_doc['comment'] = comment
1138 # - save it
1139 if not new_doc.save_payload():
1140 wx.EndBusyCursor()
1141 gmGuiHelpers.gm_show_error (
1142 aMessage = _('Cannot update document metadata.'),
1143 aTitle = _('saving document')
1144 )
1145 return False
1146 # - long description
1147 description = self._TBOX_description.GetValue().strip()
1148 if description != '':
1149 if not new_doc.add_description(description):
1150 wx.EndBusyCursor()
1151 gmGuiHelpers.gm_show_error (
1152 aMessage = _('Cannot add document description.'),
1153 aTitle = _('saving document')
1154 )
1155 return False
1156
1157 # add document parts from files
1158 success, msg, filename = new_doc.add_parts_from_files (
1159 files = self.acquired_pages,
1160 reviewer = self._PhWheel_reviewer.GetData()
1161 )
1162 if not success:
1163 wx.EndBusyCursor()
1164 gmGuiHelpers.gm_show_error (
1165 aMessage = msg,
1166 aTitle = _('saving document')
1167 )
1168 return False
1169
1170 # set reviewed status
1171 if self._ChBOX_reviewed.GetValue():
1172 if not new_doc.set_reviewed (
1173 technically_abnormal = self._ChBOX_abnormal.GetValue(),
1174 clinically_relevant = self._ChBOX_relevant.GetValue()
1175 ):
1176 msg = _('Error setting "reviewed" status of new document.')
1177
1178 gmHooks.run_hook_script(hook = u'after_new_doc_created')
1179
1180 # inform user
1181 show_id = bool (
1182 cfg.get2 (
1183 option = 'horstspace.scan_index.show_doc_id',
1184 workplace = gmSurgery.gmCurrentPractice().active_workplace,
1185 bias = 'user'
1186 )
1187 )
1188 wx.EndBusyCursor()
1189 if show_id:
1190 if ref is None:
1191 msg = _('Successfully saved the new document.')
1192 else:
1193 msg = _(
1194 """The reference ID for the new document is:
1195
1196 <%s>
1197
1198 You probably want to write it down on the
1199 original documents.
1200
1201 If you don't care about the ID you can switch
1202 off this message in the GNUmed configuration.""") % ref
1203 gmGuiHelpers.gm_show_info (
1204 aMessage = msg,
1205 aTitle = _('Saving document')
1206 )
1207 else:
1208 gmDispatcher.send(signal='statustext', msg=_('Successfully saved new document.'))
1209
1210 self.__init_ui_data()
1211 return True
1212 #--------------------------------------------------------
1215 #--------------------------------------------------------
1217 self._ChBOX_abnormal.Enable(enable = self._ChBOX_reviewed.GetValue())
1218 self._ChBOX_relevant.Enable(enable = self._ChBOX_reviewed.GetValue())
1219 #--------------------------------------------------------
1221 pk_doc_type = self._PhWheel_doc_type.GetData()
1222 if pk_doc_type is None:
1223 self._PRW_doc_comment.unset_context(context = 'pk_doc_type')
1224 else:
1225 self._PRW_doc_comment.set_context(context = 'pk_doc_type', val = pk_doc_type)
1226 return True
1227 #============================================================
1228 from Gnumed.wxGladeWidgets import wxgSelectablySortedDocTreePnl
1229
1230 -class cSelectablySortedDocTreePnl(wxgSelectablySortedDocTreePnl.wxgSelectablySortedDocTreePnl):
1231 """A panel with a document tree which can be sorted."""
1232 #--------------------------------------------------------
1233 # inherited event handlers
1234 #--------------------------------------------------------
1236 self._doc_tree.sort_mode = 'age'
1237 self._doc_tree.SetFocus()
1238 self._rbtn_sort_by_age.SetValue(True)
1239 #--------------------------------------------------------
1241 self._doc_tree.sort_mode = 'review'
1242 self._doc_tree.SetFocus()
1243 self._rbtn_sort_by_review.SetValue(True)
1244 #--------------------------------------------------------
1246 self._doc_tree.sort_mode = 'episode'
1247 self._doc_tree.SetFocus()
1248 self._rbtn_sort_by_episode.SetValue(True)
1249 #--------------------------------------------------------
1254 #============================================================
1256 # FIXME: handle expansion state
1257 """This wx.TreeCtrl derivative displays a tree view of stored medical documents.
1258
1259 It listens to document and patient changes and updated itself accordingly.
1260
1261 This acts on the current patient.
1262 """
1263 _sort_modes = ['age', 'review', 'episode', 'type']
1264 _root_node_labels = None
1265 #--------------------------------------------------------
1267 """Set up our specialised tree.
1268 """
1269 kwds['style'] = wx.TR_NO_BUTTONS | wx.NO_BORDER | wx.TR_SINGLE
1270 wx.TreeCtrl.__init__(self, parent, id, *args, **kwds)
1271
1272 gmRegetMixin.cRegetOnPaintMixin.__init__(self)
1273
1274 tmp = _('available documents (%s)')
1275 unsigned = _('unsigned (%s) on top') % u'\u270D'
1276 cDocTree._root_node_labels = {
1277 'age': tmp % _('most recent on top'),
1278 'review': tmp % unsigned,
1279 'episode': tmp % _('sorted by episode'),
1280 'type': tmp % _('sorted by type')
1281 }
1282
1283 self.root = None
1284 self.__sort_mode = 'age'
1285
1286 self.__build_context_menus()
1287 self.__register_interests()
1288 self._schedule_data_reget()
1289 #--------------------------------------------------------
1290 # external API
1291 #--------------------------------------------------------
1293
1294 node = self.GetSelection()
1295 node_data = self.GetPyData(node)
1296
1297 if not isinstance(node_data, gmDocuments.cDocumentPart):
1298 return True
1299
1300 self.__display_part(part = node_data)
1301 return True
1302 #--------------------------------------------------------
1303 # properties
1304 #--------------------------------------------------------
1307 #-----
1309 if mode is None:
1310 mode = 'age'
1311
1312 if mode == self.__sort_mode:
1313 return
1314
1315 if mode not in cDocTree._sort_modes:
1316 raise ValueError('invalid document tree sort mode [%s], valid modes: %s' % (mode, cDocTree._sort_modes))
1317
1318 self.__sort_mode = mode
1319
1320 curr_pat = gmPerson.gmCurrentPatient()
1321 if not curr_pat.connected:
1322 return
1323
1324 self._schedule_data_reget()
1325 #-----
1326 sort_mode = property(_get_sort_mode, _set_sort_mode)
1327 #--------------------------------------------------------
1328 # reget-on-paint API
1329 #--------------------------------------------------------
1331 curr_pat = gmPerson.gmCurrentPatient()
1332 if not curr_pat.connected:
1333 gmDispatcher.send(signal = 'statustext', msg = _('Cannot load documents. No active patient.'))
1334 return False
1335
1336 if not self.__populate_tree():
1337 return False
1338
1339 return True
1340 #--------------------------------------------------------
1341 # internal helpers
1342 #--------------------------------------------------------
1344 # connect handlers
1345 wx.EVT_TREE_ITEM_ACTIVATED (self, self.GetId(), self._on_activate)
1346 wx.EVT_TREE_ITEM_RIGHT_CLICK (self, self.GetId(), self.__on_right_click)
1347
1348 # wx.EVT_LEFT_DCLICK(self.tree, self.OnLeftDClick)
1349
1350 gmDispatcher.connect(signal = u'pre_patient_selection', receiver = self._on_pre_patient_selection)
1351 gmDispatcher.connect(signal = u'post_patient_selection', receiver = self._on_post_patient_selection)
1352 gmDispatcher.connect(signal = u'doc_mod_db', receiver = self._on_doc_mod_db)
1353 gmDispatcher.connect(signal = u'doc_page_mod_db', receiver = self._on_doc_page_mod_db)
1354 #--------------------------------------------------------
1432
1433 # document / description
1434 # self.__desc_menu = wx.Menu()
1435 # ID = wx.NewId()
1436 # self.__doc_context_menu.AppendMenu(ID, _('Descriptions ...'), self.__desc_menu)
1437
1438 # ID = wx.NewId()
1439 # self.__desc_menu.Append(ID, _('Add new description'))
1440 # wx.EVT_MENU(self.__desc_menu, ID, self.__add_doc_desc)
1441
1442 # ID = wx.NewId()
1443 # self.__desc_menu.Append(ID, _('Delete description'))
1444 # wx.EVT_MENU(self.__desc_menu, ID, self.__del_doc_desc)
1445
1446 # self.__desc_menu.AppendSeparator()
1447 #--------------------------------------------------------
1449
1450 wx.BeginBusyCursor()
1451
1452 # clean old tree
1453 if self.root is not None:
1454 self.DeleteAllItems()
1455
1456 # init new tree
1457 self.root = self.AddRoot(cDocTree._root_node_labels[self.__sort_mode], -1, -1)
1458 self.SetItemPyData(self.root, None)
1459 self.SetItemHasChildren(self.root, False)
1460
1461 # read documents from database
1462 curr_pat = gmPerson.gmCurrentPatient()
1463 docs_folder = curr_pat.get_document_folder()
1464 docs = docs_folder.get_documents()
1465
1466 if docs is None:
1467 gmGuiHelpers.gm_show_error (
1468 aMessage = _('Error searching documents.'),
1469 aTitle = _('loading document list')
1470 )
1471 # avoid recursion of GUI updating
1472 wx.EndBusyCursor()
1473 return True
1474
1475 if len(docs) == 0:
1476 wx.EndBusyCursor()
1477 return True
1478
1479 # fill new tree from document list
1480 self.SetItemHasChildren(self.root, True)
1481
1482 # add our documents as first level nodes
1483 intermediate_nodes = {}
1484 for doc in docs:
1485
1486 parts = doc.parts
1487
1488 label = _('%s%7s %s:%s (%s part(s)%s)') % (
1489 gmTools.bool2subst(doc.has_unreviewed_parts, gmTools.u_writing_hand, u'', u'?'),
1490 doc['clin_when'].strftime('%m/%Y'),
1491 doc['l10n_type'][:26],
1492 gmTools.coalesce(initial = doc['comment'], instead = u'', template_initial = u' %s'),
1493 len(parts),
1494 gmTools.coalesce(initial = doc['ext_ref'], instead = u'', template_initial = u', \u00BB%s\u00AB')
1495 )
1496
1497 # need intermediate branch level ?
1498 if self.__sort_mode == 'episode':
1499 lbl = doc['episode'] # it'd be nice to also show the issue but we don't have that
1500 if not intermediate_nodes.has_key(lbl):
1501 intermediate_nodes[lbl] = self.AppendItem(parent = self.root, text = lbl)
1502 self.SetItemBold(intermediate_nodes[lbl], bold = True)
1503 self.SetItemPyData(intermediate_nodes[lbl], None)
1504 self.SetItemHasChildren(intermediate_nodes[lbl], True)
1505 parent = intermediate_nodes[lbl]
1506 elif self.__sort_mode == 'type':
1507 if not intermediate_nodes.has_key(doc['l10n_type']):
1508 intermediate_nodes[doc['l10n_type']] = self.AppendItem(parent = self.root, text = doc['l10n_type'])
1509 self.SetItemBold(intermediate_nodes[doc['l10n_type']], bold = True)
1510 self.SetItemPyData(intermediate_nodes[doc['l10n_type']], None)
1511 self.SetItemHasChildren(intermediate_nodes[doc['l10n_type']], True)
1512 parent = intermediate_nodes[doc['l10n_type']]
1513 else:
1514 parent = self.root
1515
1516 doc_node = self.AppendItem(parent = parent, text = label)
1517 #self.SetItemBold(doc_node, bold = True)
1518 self.SetItemPyData(doc_node, doc)
1519 if len(parts) == 0:
1520 self.SetItemHasChildren(doc_node, False)
1521 else:
1522 self.SetItemHasChildren(doc_node, True)
1523
1524 # now add parts as child nodes
1525 for part in parts:
1526 # if part['clinically_relevant']:
1527 # rel = ' [%s]' % _('Cave')
1528 # else:
1529 # rel = ''
1530 f_ext = u''
1531 if part['filename'] is not None:
1532 f_ext = os.path.splitext(part['filename'])[1].strip('.').strip()
1533 if f_ext != u'':
1534 f_ext = u' .' + f_ext.upper()
1535 label = '%s%s (%s%s)%s' % (
1536 gmTools.bool2str (
1537 boolean = part['reviewed'] or part['reviewed_by_you'] or part['reviewed_by_intended_reviewer'],
1538 true_str = u'',
1539 false_str = gmTools.u_writing_hand
1540 ),
1541 _('part %2s') % part['seq_idx'],
1542 gmTools.size2str(part['size']),
1543 f_ext,
1544 gmTools.coalesce (
1545 part['obj_comment'],
1546 u'',
1547 u': %s%%s%s' % (gmTools.u_left_double_angle_quote, gmTools.u_right_double_angle_quote)
1548 )
1549 )
1550
1551 part_node = self.AppendItem(parent = doc_node, text = label)
1552 self.SetItemPyData(part_node, part)
1553 self.SetItemHasChildren(part_node, False)
1554
1555 self.__sort_nodes()
1556 self.SelectItem(self.root)
1557
1558 # FIXME: apply expansion state if available or else ...
1559 # FIXME: ... uncollapse to default state
1560 self.Expand(self.root)
1561 if self.__sort_mode in ['episode', 'type']:
1562 for key in intermediate_nodes.keys():
1563 self.Expand(intermediate_nodes[key])
1564
1565 wx.EndBusyCursor()
1566
1567 return True
1568 #------------------------------------------------------------------------
1570 """Used in sorting items.
1571
1572 -1: 1 < 2
1573 0: 1 = 2
1574 1: 1 > 2
1575 """
1576 # Windows can send bogus events so ignore that
1577 if not node1:
1578 _log.debug('invalid node 1')
1579 return 0
1580 if not node2:
1581 _log.debug('invalid node 2')
1582 return 0
1583 if not node1.IsOk():
1584 _log.debug('no data on node 1')
1585 return 0
1586 if not node2.IsOk():
1587 _log.debug('no data on node 2')
1588 return 0
1589
1590 data1 = self.GetPyData(node1)
1591 data2 = self.GetPyData(node2)
1592
1593 # doc node
1594 if isinstance(data1, gmDocuments.cDocument):
1595
1596 date_field = 'clin_when'
1597 #date_field = 'modified_when'
1598
1599 if self.__sort_mode == 'age':
1600 # reverse sort by date
1601 if data1[date_field] > data2[date_field]:
1602 return -1
1603 if data1[date_field] == data2[date_field]:
1604 return 0
1605 return 1
1606
1607 elif self.__sort_mode == 'episode':
1608 if data1['episode'] < data2['episode']:
1609 return -1
1610 if data1['episode'] == data2['episode']:
1611 # inner sort: reverse by date
1612 if data1[date_field] > data2[date_field]:
1613 return -1
1614 if data1[date_field] == data2[date_field]:
1615 return 0
1616 return 1
1617 return 1
1618
1619 elif self.__sort_mode == 'review':
1620 # equality
1621 if data1.has_unreviewed_parts == data2.has_unreviewed_parts:
1622 # inner sort: reverse by date
1623 if data1[date_field] > data2[date_field]:
1624 return -1
1625 if data1[date_field] == data2[date_field]:
1626 return 0
1627 return 1
1628 if data1.has_unreviewed_parts:
1629 return -1
1630 return 1
1631
1632 elif self.__sort_mode == 'type':
1633 if data1['l10n_type'] < data2['l10n_type']:
1634 return -1
1635 if data1['l10n_type'] == data2['l10n_type']:
1636 # inner sort: reverse by date
1637 if data1[date_field] > data2[date_field]:
1638 return -1
1639 if data1[date_field] == data2[date_field]:
1640 return 0
1641 return 1
1642 return 1
1643
1644 else:
1645 _log.error('unknown document sort mode [%s], reverse-sorting by age', self.__sort_mode)
1646 # reverse sort by date
1647 if data1[date_field] > data2[date_field]:
1648 return -1
1649 if data1[date_field] == data2[date_field]:
1650 return 0
1651 return 1
1652
1653 # part node
1654 if isinstance(data1, gmDocuments.cDocumentPart):
1655 # compare sequence IDs (= "page" numbers)
1656 # FIXME: wrong order ?
1657 if data1['seq_idx'] < data2['seq_idx']:
1658 return -1
1659 if data1['seq_idx'] == data2['seq_idx']:
1660 return 0
1661 return 1
1662
1663 # else sort alphabetically
1664 if None in [data1, data2]:
1665 l1 = self.GetItemText(node1)
1666 l2 = self.GetItemText(node2)
1667 if l1 < l2:
1668 return -1
1669 if l1 == l2:
1670 return 0
1671 else:
1672 if data1 < data2:
1673 return -1
1674 if data1 == data2:
1675 return 0
1676 return 1
1677 #------------------------------------------------------------------------
1678 # event handlers
1679 #------------------------------------------------------------------------
1683 #------------------------------------------------------------------------
1687 #------------------------------------------------------------------------
1689 # FIXME: self.__store_expansion_history_in_db
1690
1691 # empty out tree
1692 if self.root is not None:
1693 self.DeleteAllItems()
1694 self.root = None
1695 #------------------------------------------------------------------------
1697 # FIXME: self.__load_expansion_history_from_db (but not apply it !)
1698 self._schedule_data_reget()
1699 #------------------------------------------------------------------------
1701 node = event.GetItem()
1702 node_data = self.GetPyData(node)
1703
1704 # exclude pseudo root node
1705 if node_data is None:
1706 return None
1707
1708 # expand/collapse documents on activation
1709 if isinstance(node_data, gmDocuments.cDocument):
1710 self.Toggle(node)
1711 return True
1712
1713 # string nodes are labels such as episodes which may or may not have children
1714 if type(node_data) == type('string'):
1715 self.Toggle(node)
1716 return True
1717
1718 self.__display_part(part = node_data)
1719 return True
1720 #--------------------------------------------------------
1722
1723 node = evt.GetItem()
1724 self.__curr_node_data = self.GetPyData(node)
1725
1726 # exclude pseudo root node
1727 if self.__curr_node_data is None:
1728 return None
1729
1730 # documents
1731 if isinstance(self.__curr_node_data, gmDocuments.cDocument):
1732 self.__handle_doc_context()
1733
1734 # parts
1735 if isinstance(self.__curr_node_data, gmDocuments.cDocumentPart):
1736 self.__handle_part_context()
1737
1738 del self.__curr_node_data
1739 evt.Skip()
1740 #--------------------------------------------------------
1742 self.__curr_node_data.set_as_active_photograph()
1743 #--------------------------------------------------------
1746 #--------------------------------------------------------
1749 #--------------------------------------------------------
1751 manage_document_descriptions(parent = self, document = self.__curr_node_data)
1752 #--------------------------------------------------------
1753 # internal API
1754 #--------------------------------------------------------
1756
1757 if start_node is None:
1758 start_node = self.GetRootItem()
1759
1760 # protect against empty tree where not even
1761 # a root node exists
1762 if not start_node.IsOk():
1763 return True
1764
1765 self.SortChildren(start_node)
1766
1767 child_node, cookie = self.GetFirstChild(start_node)
1768 while child_node.IsOk():
1769 self.__sort_nodes(start_node = child_node)
1770 child_node, cookie = self.GetNextChild(start_node, cookie)
1771
1772 return
1773 #--------------------------------------------------------
1776 #--------------------------------------------------------
1778
1779 # make active patient photograph
1780 if self.__curr_node_data['type'] == 'patient photograph':
1781 ID = wx.NewId()
1782 self.__part_context_menu.Append(ID, _('Activate as current photo'))
1783 wx.EVT_MENU(self.__part_context_menu, ID, self.__activate_as_current_photo)
1784 else:
1785 ID = None
1786
1787 self.PopupMenu(self.__part_context_menu, wx.DefaultPosition)
1788
1789 if ID is not None:
1790 self.__part_context_menu.Delete(ID)
1791 #--------------------------------------------------------
1792 # part level context menu handlers
1793 #--------------------------------------------------------
1795 """Display document part."""
1796
1797 # sanity check
1798 if part['size'] == 0:
1799 _log.debug('cannot display part [%s] - 0 bytes', part['pk_obj'])
1800 gmGuiHelpers.gm_show_error (
1801 aMessage = _('Document part does not seem to exist in database !'),
1802 aTitle = _('showing document')
1803 )
1804 return None
1805
1806 wx.BeginBusyCursor()
1807
1808 cfg = gmCfg.cCfgSQL()
1809
1810 # # get export directory for temporary files
1811 # tmp_dir = gmTools.coalesce (
1812 # cfg.get2 (
1813 # option = "horstspace.tmp_dir",
1814 # workplace = gmSurgery.gmCurrentPractice().active_workplace,
1815 # bias = 'workplace'
1816 # ),
1817 # os.path.expanduser(os.path.join('~', '.gnumed', 'tmp'))
1818 # )
1819 # _log.debug("temporary directory [%s]", tmp_dir)
1820
1821 # determine database export chunk size
1822 chunksize = int(
1823 cfg.get2 (
1824 option = "horstspace.blob_export_chunk_size",
1825 workplace = gmSurgery.gmCurrentPractice().active_workplace,
1826 bias = 'workplace',
1827 default = default_chunksize
1828 ))
1829
1830 # shall we force blocking during view ?
1831 block_during_view = bool( cfg.get2 (
1832 option = 'horstspace.document_viewer.block_during_view',
1833 workplace = gmSurgery.gmCurrentPractice().active_workplace,
1834 bias = 'user',
1835 default = None
1836 ))
1837
1838 # display it
1839 successful, msg = part.display_via_mime (
1840 # tmpdir = tmp_dir,
1841 chunksize = chunksize,
1842 block = block_during_view
1843 )
1844
1845 wx.EndBusyCursor()
1846
1847 if not successful:
1848 gmGuiHelpers.gm_show_error (
1849 aMessage = _('Cannot display document part:\n%s') % msg,
1850 aTitle = _('showing document')
1851 )
1852 return None
1853
1854 # handle review after display
1855 # 0: never
1856 # 1: always
1857 # 2: if no review by myself exists yet
1858 # 3: if no review at all exists yet
1859 # 4: if no review by responsible reviewer
1860 review_after_display = int(cfg.get2 (
1861 option = 'horstspace.document_viewer.review_after_display',
1862 workplace = gmSurgery.gmCurrentPractice().active_workplace,
1863 bias = 'user',
1864 default = 3
1865 ))
1866 if review_after_display == 1: # always review
1867 self.__review_part(part=part)
1868 elif review_after_display == 2: # review if no review by me exists
1869 review_by_me = filter(lambda rev: rev['is_your_review'], part.get_reviews())
1870 if len(review_by_me) == 0:
1871 self.__review_part(part = part)
1872 elif review_after_display == 3:
1873 if len(part.get_reviews()) == 0:
1874 self.__review_part(part = part)
1875 elif review_after_display == 4:
1876 reviewed_by_responsible = filter(lambda rev: rev['is_review_by_responsible_reviewer'], part.get_reviews())
1877 if len(reviewed_by_responsible) == 0:
1878 self.__review_part(part = part)
1879
1880 return True
1881 #--------------------------------------------------------
1883 dlg = cReviewDocPartDlg (
1884 parent = self,
1885 id = -1,
1886 part = part
1887 )
1888 dlg.ShowModal()
1889 dlg.Destroy()
1890 #--------------------------------------------------------
1892
1893 gmHooks.run_hook_script(hook = u'before_%s_doc_part' % action)
1894
1895 wx.BeginBusyCursor()
1896
1897 # detect wrapper
1898 found, external_cmd = gmShellAPI.detect_external_binary(u'gm-%s_doc' % action)
1899 if not found:
1900 found, external_cmd = gmShellAPI.detect_external_binary(u'gm-%s_doc.bat' % action)
1901 if not found:
1902 _log.error('neither of gm-%s_doc or gm-%s_doc.bat found', action, action)
1903 wx.EndBusyCursor()
1904 gmGuiHelpers.gm_show_error (
1905 _('Cannot %(l10n_action)s document part - %(l10n_action)s command not found.\n'
1906 '\n'
1907 'Either of gm_%(action)s_doc.sh or gm_%(action)s_doc.bat\n'
1908 'must be in the execution path. The command will\n'
1909 'be passed the filename to %(l10n_action)s.'
1910 ) % {'action': action, 'l10n_action': l10n_action},
1911 _('Processing document part: %s') % l10n_action
1912 )
1913 return
1914
1915 cfg = gmCfg.cCfgSQL()
1916
1917 # # get export directory for temporary files
1918 # tmp_dir = gmTools.coalesce (
1919 # cfg.get2 (
1920 # option = "horstspace.tmp_dir",
1921 # workplace = gmSurgery.gmCurrentPractice().active_workplace,
1922 # bias = 'workplace'
1923 # ),
1924 # os.path.expanduser(os.path.join('~', '.gnumed', 'tmp'))
1925 # )
1926 # _log.debug("temporary directory [%s]", tmp_dir)
1927
1928 # determine database export chunk size
1929 chunksize = int(cfg.get2 (
1930 option = "horstspace.blob_export_chunk_size",
1931 workplace = gmSurgery.gmCurrentPractice().active_workplace,
1932 bias = 'workplace',
1933 default = default_chunksize
1934 ))
1935
1936 part_file = self.__curr_node_data.export_to_file (
1937 # aTempDir = tmp_dir,
1938 aChunkSize = chunksize
1939 )
1940
1941 cmd = u'%s %s' % (external_cmd, part_file)
1942 success = gmShellAPI.run_command_in_shell (
1943 command = cmd,
1944 blocking = False
1945 )
1946
1947 wx.EndBusyCursor()
1948
1949 if not success:
1950 _log.error('%s command failed: [%s]', action, cmd)
1951 gmGuiHelpers.gm_show_error (
1952 _('Cannot %(l10n_action)s document part - %(l10n_action)s command failed.\n'
1953 '\n'
1954 'You may need to check and fix either of\n'
1955 ' gm_%(action)s_doc.sh (Unix/Mac) or\n'
1956 ' gm_%(action)s_doc.bat (Windows)\n'
1957 '\n'
1958 'The command is passed the filename to %(l10n_action)s.'
1959 ) % {'action': action, 'l10n_action': l10n_action},
1960 _('Processing document part: %s') % l10n_action
1961 )
1962 #--------------------------------------------------------
1963 # FIXME: icons in the plugin toolbar
1965 self.__process_part(action = u'print', l10n_action = _('print'))
1966 #--------------------------------------------------------
1968 self.__process_part(action = u'fax', l10n_action = _('fax'))
1969 #--------------------------------------------------------
1971 self.__process_part(action = u'mail', l10n_action = _('mail'))
1972 #--------------------------------------------------------
1973 # document level context menu handlers
1974 #--------------------------------------------------------
1976 enc = gmEMRStructWidgets.select_encounters (
1977 parent = self,
1978 patient = gmPerson.gmCurrentPatient()
1979 )
1980 if not enc:
1981 return
1982 self.__curr_node_data['pk_encounter'] = enc['pk_encounter']
1983 self.__curr_node_data.save()
1984 #--------------------------------------------------------
1986 enc = gmEMRStructItems.cEncounter(aPK_obj = self.__curr_node_data['pk_encounter'])
1987 gmEMRStructWidgets.edit_encounter(parent = self, encounter = enc)
1988 #--------------------------------------------------------
1990
1991 gmHooks.run_hook_script(hook = u'before_%s_doc' % action)
1992
1993 wx.BeginBusyCursor()
1994
1995 # detect wrapper
1996 found, external_cmd = gmShellAPI.detect_external_binary(u'gm-%s_doc' % action)
1997 if not found:
1998 found, external_cmd = gmShellAPI.detect_external_binary(u'gm-%s_doc.bat' % action)
1999 if not found:
2000 _log.error('neither of gm-%s_doc or gm-%s_doc.bat found', action, action)
2001 wx.EndBusyCursor()
2002 gmGuiHelpers.gm_show_error (
2003 _('Cannot %(l10n_action)s document - %(l10n_action)s command not found.\n'
2004 '\n'
2005 'Either of gm_%(action)s_doc.sh or gm_%(action)s_doc.bat\n'
2006 'must be in the execution path. The command will\n'
2007 'be passed a list of filenames to %(l10n_action)s.'
2008 ) % {'action': action, 'l10n_action': l10n_action},
2009 _('Processing document: %s') % l10n_action
2010 )
2011 return
2012
2013 cfg = gmCfg.cCfgSQL()
2014
2015 # # get export directory for temporary files
2016 # tmp_dir = gmTools.coalesce (
2017 # cfg.get2 (
2018 # option = "horstspace.tmp_dir",
2019 # workplace = gmSurgery.gmCurrentPractice().active_workplace,
2020 # bias = 'workplace'
2021 # ),
2022 # os.path.expanduser(os.path.join('~', '.gnumed', 'tmp'))
2023 # )
2024 # _log.debug("temporary directory [%s]", tmp_dir)
2025
2026 # determine database export chunk size
2027 chunksize = int(cfg.get2 (
2028 option = "horstspace.blob_export_chunk_size",
2029 workplace = gmSurgery.gmCurrentPractice().active_workplace,
2030 bias = 'workplace',
2031 default = default_chunksize
2032 ))
2033
2034 part_files = self.__curr_node_data.export_parts_to_files (
2035 # export_dir = tmp_dir,
2036 chunksize = chunksize
2037 )
2038
2039 cmd = external_cmd + u' ' + u' '.join(part_files)
2040 success = gmShellAPI.run_command_in_shell (
2041 command = cmd,
2042 blocking = False
2043 )
2044
2045 wx.EndBusyCursor()
2046
2047 if not success:
2048 _log.error('%s command failed: [%s]', action, cmd)
2049 gmGuiHelpers.gm_show_error (
2050 _('Cannot %(l10n_action)s document - %(l10n_action)s command failed.\n'
2051 '\n'
2052 'You may need to check and fix either of\n'
2053 ' gm_%(action)s_doc.sh (Unix/Mac) or\n'
2054 ' gm_%(action)s_doc.bat (Windows)\n'
2055 '\n'
2056 'The command is passed a list of filenames to %(l10n_action)s.'
2057 ) % {'action': action, 'l10n_action': l10n_action},
2058 _('Processing document: %s') % l10n_action
2059 )
2060 #--------------------------------------------------------
2061 # FIXME: icons in the plugin toolbar
2063 self.__process_doc(action = u'print', l10n_action = _('print'))
2064 #--------------------------------------------------------
2066 self.__process_doc(action = u'fax', l10n_action = _('fax'))
2067 #--------------------------------------------------------
2069 self.__process_doc(action = u'mail', l10n_action = _('mail'))
2070 #--------------------------------------------------------
2072
2073 gmHooks.run_hook_script(hook = u'before_external_doc_access')
2074
2075 wx.BeginBusyCursor()
2076
2077 # detect wrapper
2078 found, external_cmd = gmShellAPI.detect_external_binary(u'gm_access_external_doc.sh')
2079 if not found:
2080 found, external_cmd = gmShellAPI.detect_external_binary(u'gm_access_external_doc.bat')
2081 if not found:
2082 _log.error('neither of gm_access_external_doc.sh or .bat found')
2083 wx.EndBusyCursor()
2084 gmGuiHelpers.gm_show_error (
2085 _('Cannot access external document - access command not found.\n'
2086 '\n'
2087 'Either of gm_access_external_doc.sh or *.bat must be\n'
2088 'in the execution path. The command will be passed the\n'
2089 'document type and the reference URL for processing.'
2090 ),
2091 _('Accessing external document')
2092 )
2093 return
2094
2095 cmd = u'%s "%s" "%s"' % (external_cmd, self.__curr_node_data['type'], self.__curr_node_data['ext_ref'])
2096 success = gmShellAPI.run_command_in_shell (
2097 command = cmd,
2098 blocking = False
2099 )
2100
2101 wx.EndBusyCursor()
2102
2103 if not success:
2104 _log.error('External access command failed: [%s]', cmd)
2105 gmGuiHelpers.gm_show_error (
2106 _('Cannot access external document - access command failed.\n'
2107 '\n'
2108 'You may need to check and fix either of\n'
2109 ' gm_access_external_doc.sh (Unix/Mac) or\n'
2110 ' gm_access_external_doc.bat (Windows)\n'
2111 '\n'
2112 'The command is passed the document type and the\n'
2113 'external reference URL on the command line.'
2114 ),
2115 _('Accessing external document')
2116 )
2117 #--------------------------------------------------------
2119 """Export document into directory.
2120
2121 - one file per object
2122 - into subdirectory named after patient
2123 """
2124 pat = gmPerson.gmCurrentPatient()
2125 dname = '%s-%s%s' % (
2126 self.__curr_node_data['l10n_type'],
2127 self.__curr_node_data['clin_when'].strftime('%Y-%m-%d'),
2128 gmTools.coalesce(self.__curr_node_data['ext_ref'], '', '-%s').replace(' ', '_')
2129 )
2130 def_dir = os.path.expanduser(os.path.join('~', 'gnumed', 'export', 'docs', pat['dirname'], dname))
2131 gmTools.mkdir(def_dir)
2132
2133 dlg = wx.DirDialog (
2134 parent = self,
2135 message = _('Save document into directory ...'),
2136 defaultPath = def_dir,
2137 style = wx.DD_DEFAULT_STYLE
2138 )
2139 result = dlg.ShowModal()
2140 dirname = dlg.GetPath()
2141 dlg.Destroy()
2142
2143 if result != wx.ID_OK:
2144 return True
2145
2146 wx.BeginBusyCursor()
2147
2148 cfg = gmCfg.cCfgSQL()
2149
2150 # determine database export chunk size
2151 chunksize = int(cfg.get2 (
2152 option = "horstspace.blob_export_chunk_size",
2153 workplace = gmSurgery.gmCurrentPractice().active_workplace,
2154 bias = 'workplace',
2155 default = default_chunksize
2156 ))
2157
2158 fnames = self.__curr_node_data.export_parts_to_files(export_dir = dirname, chunksize = chunksize)
2159
2160 wx.EndBusyCursor()
2161
2162 gmDispatcher.send(signal='statustext', msg=_('Successfully exported %s parts into the directory [%s].') % (len(fnames), dirname))
2163
2164 return True
2165 #--------------------------------------------------------
2167 result = gmGuiHelpers.gm_show_question (
2168 aMessage = _('Are you sure you want to delete the document ?'),
2169 aTitle = _('Deleting document')
2170 )
2171 if result is True:
2172 curr_pat = gmPerson.gmCurrentPatient()
2173 emr = curr_pat.get_emr()
2174 enc = emr.active_encounter
2175 gmDocuments.delete_document(document_id = self.__curr_node_data['pk_doc'], encounter_id = enc['pk_encounter'])
2176 #============================================================
2177 # main
2178 #------------------------------------------------------------
2179 if __name__ == '__main__':
2180
2181 gmI18N.activate_locale()
2182 gmI18N.install_domain(domain = 'gnumed')
2183
2184 #----------------------------------------
2185 #----------------------------------------
2186 if (len(sys.argv) > 1) and (sys.argv[1] == 'test'):
2187 # test_*()
2188 pass
2189
2190 #============================================================
2191
| Home | Trees | Indices | Help |
|
|---|
| Generated by Epydoc 3.0.1 on Mon Dec 5 04:00:02 2011 | http://epydoc.sourceforge.net |