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