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