| Home | Trees | Indices | Help |
|
|---|
|
|
1 # -*- coding: utf-8 -*-
2 #============================================================
3
4
5 __doc__ = """GNUmed medical document handling widgets."""
6
7 __license__ = "GPL v2 or later"
8 __author__ = "Karsten Hilbert <Karsten.Hilbert@gmx.net>"
9
10 #============================================================
11 import os.path
12 import os
13 import sys
14 import re as regex
15 import logging
16
17
18 import wx
19 import wx.lib.mixins.treemixin as treemixin
20
21
22 if __name__ == '__main__':
23 sys.path.insert(0, '../../')
24 from Gnumed.pycommon import gmI18N
25 if __name__ == '__main__':
26 gmI18N.activate_locale()
27 gmI18N.install_domain(domain = 'gnumed')
28 from Gnumed.pycommon import gmCfg
29 from Gnumed.pycommon import gmPG2
30 from Gnumed.pycommon import gmMimeLib
31 from Gnumed.pycommon import gmMatchProvider
32 from Gnumed.pycommon import gmDispatcher
33 from Gnumed.pycommon import gmDateTime
34 from Gnumed.pycommon import gmTools
35 from Gnumed.pycommon import gmShellAPI
36 from Gnumed.pycommon import gmHooks
37 from Gnumed.pycommon import gmNetworkTools
38 from Gnumed.pycommon import gmMimeLib
39
40 from Gnumed.business import gmPerson
41 from Gnumed.business import gmStaff
42 from Gnumed.business import gmDocuments
43 from Gnumed.business import gmEMRStructItems
44 from Gnumed.business import gmPraxis
45 from Gnumed.business import gmDICOM
46 from Gnumed.business import gmProviderInbox
47
48 from Gnumed.wxpython import gmGuiHelpers
49 from Gnumed.wxpython import gmRegetMixin
50 from Gnumed.wxpython import gmPhraseWheel
51 from Gnumed.wxpython import gmPlugin
52 from Gnumed.wxpython import gmEncounterWidgets
53 from Gnumed.wxpython import gmListWidgets
54 from Gnumed.wxpython import gmRegetMixin
55
56
57 _log = logging.getLogger('gm.ui')
58
59
60 default_chunksize = 1 * 1024 * 1024 # 1 MB
61
62 #============================================================
64
65 #-----------------------------------
66 def delete_item(item):
67 doit = gmGuiHelpers.gm_show_question (
68 _( 'Are you sure you want to delete this\n'
69 'description from the document ?\n'
70 ),
71 _('Deleting document description')
72 )
73 if not doit:
74 return True
75
76 document.delete_description(pk = item[0])
77 return True
78 #-----------------------------------
79 def add_item():
80 dlg = gmGuiHelpers.cMultilineTextEntryDlg (
81 parent,
82 -1,
83 title = _('Adding document description'),
84 msg = _('Below you can add a document description.\n')
85 )
86 result = dlg.ShowModal()
87 if result == wx.ID_SAVE:
88 document.add_description(dlg.value)
89
90 dlg.Destroy()
91 return True
92 #-----------------------------------
93 def edit_item(item):
94 dlg = gmGuiHelpers.cMultilineTextEntryDlg (
95 parent,
96 -1,
97 title = _('Editing document description'),
98 msg = _('Below you can edit the document description.\n'),
99 text = item[1]
100 )
101 result = dlg.ShowModal()
102 if result == wx.ID_SAVE:
103 document.update_description(pk = item[0], description = dlg.value)
104
105 dlg.Destroy()
106 return True
107 #-----------------------------------
108 def refresh_list(lctrl):
109 descriptions = document.get_descriptions()
110
111 lctrl.set_string_items(items = [
112 '%s%s' % ( (' '.join(regex.split('\r\n+|\r+|\n+|\t+', desc[1])))[:30], gmTools.u_ellipsis )
113 for desc in descriptions
114 ])
115 lctrl.set_data(data = descriptions)
116 #-----------------------------------
117
118 gmListWidgets.get_choices_from_list (
119 parent = parent,
120 msg = _('Select the description you are interested in.\n'),
121 caption = _('Managing document descriptions'),
122 columns = [_('Description')],
123 edit_callback = edit_item,
124 new_callback = add_item,
125 delete_callback = delete_item,
126 refresh_callback = refresh_list,
127 single_selection = True,
128 can_return_empty = True
129 )
130
131 return True
132
133 #============================================================
135 try:
136 del kwargs['signal']
137 del kwargs['sender']
138 except KeyError:
139 pass
140 wx.CallAfter(save_file_as_new_document, **kwargs)
141
143 try:
144 del kwargs['signal']
145 del kwargs['sender']
146 except KeyError:
147 pass
148 wx.CallAfter(save_files_as_new_document, **kwargs)
149 #----------------------
150 -def save_file_as_new_document(parent=None, filename=None, document_type=None, unlock_patient=False, episode=None, review_as_normal=False, pk_org_unit=None):
151 return save_files_as_new_document (
152 parent = parent,
153 filenames = [filename],
154 document_type = document_type,
155 unlock_patient = unlock_patient,
156 episode = episode,
157 review_as_normal = review_as_normal,
158 pk_org_unit = pk_org_unit
159 )
160
161 #----------------------
162 -def save_files_as_new_document(parent=None, filenames=None, document_type=None, unlock_patient=False, episode=None, review_as_normal=False, reference=None, pk_org_unit=None, date_generated=None, comment=None, reviewer=None, pk_document_type=None):
163
164 pat = gmPerson.gmCurrentPatient()
165 if not pat.connected:
166 return None
167
168 emr = pat.emr
169
170 if parent is None:
171 parent = wx.GetApp().GetTopWindow()
172
173 if episode is None:
174 all_epis = emr.get_episodes()
175 # FIXME: what to do here ? probably create dummy episode
176 if len(all_epis) == 0:
177 episode = emr.add_episode(episode_name = _('Documents'), is_open = False)
178 else:
179 from Gnumed.wxpython.gmEMRStructWidgets import cEpisodeListSelectorDlg
180 dlg = cEpisodeListSelectorDlg(parent, -1, episodes = all_epis)
181 dlg.SetTitle(_('Select the episode under which to file the document ...'))
182 btn_pressed = dlg.ShowModal()
183 episode = dlg.get_selected_item_data(only_one = True)
184 dlg.Destroy()
185
186 if (btn_pressed == wx.ID_CANCEL) or (episode is None):
187 if unlock_patient:
188 pat.locked = False
189 return None
190
191 wx.BeginBusyCursor()
192
193 if pk_document_type is None:
194 pk_document_type = gmDocuments.create_document_type(document_type = document_type)['pk_doc_type']
195
196 docs_folder = pat.get_document_folder()
197 doc = docs_folder.add_document (
198 document_type = pk_document_type,
199 encounter = emr.active_encounter['pk_encounter'],
200 episode = episode['pk_episode']
201 )
202 if doc is None:
203 wx.EndBusyCursor()
204 gmGuiHelpers.gm_show_error (
205 aMessage = _('Cannot create new document.'),
206 aTitle = _('saving document')
207 )
208 return False
209
210 if reference is not None:
211 doc['ext_ref'] = reference
212 if pk_org_unit is not None:
213 doc['pk_org_unit'] = pk_org_unit
214 if date_generated is not None:
215 doc['clin_when'] = date_generated
216 if comment is not None:
217 if comment != '':
218 doc['comment'] = comment
219 doc.save()
220
221 success, msg, filename = doc.add_parts_from_files(files = filenames, reviewer = reviewer)
222 if not success:
223 wx.EndBusyCursor()
224 gmGuiHelpers.gm_show_error (
225 aMessage = msg,
226 aTitle = _('saving document')
227 )
228 return False
229
230 if review_as_normal:
231 doc.set_reviewed(technically_abnormal = False, clinically_relevant = False)
232
233 if unlock_patient:
234 pat.locked = False
235
236 gmDispatcher.send(signal = 'statustext', msg = _('Imported new document from %s.') % filenames, beep = True)
237
238 # inform user
239 cfg = gmCfg.cCfgSQL()
240 show_id = bool (
241 cfg.get2 (
242 option = 'horstspace.scan_index.show_doc_id',
243 workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace,
244 bias = 'user'
245 )
246 )
247
248 wx.EndBusyCursor()
249
250 if not show_id:
251 gmDispatcher.send(signal = 'statustext', msg = _('Successfully saved new document.'))
252 else:
253 if reference is None:
254 msg = _('Successfully saved the new document.')
255 else:
256 msg = _('The reference ID for the new document is:\n'
257 '\n'
258 ' <%s>\n'
259 '\n'
260 'You probably want to write it down on the\n'
261 'original documents.\n'
262 '\n'
263 "If you don't care about the ID you can switch\n"
264 'off this message in the GNUmed configuration.\n'
265 ) % reference
266 gmGuiHelpers.gm_show_info (
267 aMessage = msg,
268 aTitle = _('Saving document')
269 )
270
271 # remove non-temp files
272 tmp_dir = gmTools.gmPaths().tmp_dir
273 files2remove = [ f for f in filenames if not f.startswith(tmp_dir) ]
274 if len(files2remove) > 0:
275 do_delete = gmGuiHelpers.gm_show_question (
276 _( 'Successfully imported files as document.\n'
277 '\n'
278 'Do you want to delete imported files from the filesystem ?\n'
279 '\n'
280 ' %s'
281 ) % '\n '.join(files2remove),
282 _('Removing files')
283 )
284 if do_delete:
285 for fname in files2remove:
286 gmTools.remove_file(fname)
287
288 return doc
289
290 #----------------------
291 gmDispatcher.connect(signal = 'import_document_from_file', receiver = _save_file_as_new_document)
292 gmDispatcher.connect(signal = 'import_document_from_files', receiver = _save_files_as_new_document)
293
294 #============================================================
296
298
299 gmPhraseWheel.cPhraseWheel.__init__(self, *args, **kwargs)
300
301 ctxt = {'ctxt_pat': {
302 'where_part': '(pk_patient = %(pat)s) AND',
303 'placeholder': 'pat'
304 }}
305
306 mp = gmMatchProvider.cMatchProvider_SQL2 (
307 queries = ["""
308 SELECT DISTINCT ON (list_label)
309 pk_doc AS data,
310 l10n_type || ' (' || to_char(clin_when, 'YYYY Mon DD') || ')' || coalesce(': ' || unit || '@' || organization, '') || ' - ' || episode || coalesce(' (' || health_issue || ')', '') AS list_label,
311 l10n_type || ' (' || to_char(clin_when, 'YYYY Mon DD') || ')' || coalesce(': ' || organization, '') || ' - ' || coalesce(' (' || health_issue || ')', episode) AS field_label
312 FROM blobs.v_doc_med
313 WHERE
314 %(ctxt_pat)s
315 (
316 l10n_type %(fragment_condition)s
317 OR
318 unit %(fragment_condition)s
319 OR
320 organization %(fragment_condition)s
321 OR
322 episode %(fragment_condition)s
323 OR
324 health_issue %(fragment_condition)s
325 )
326 ORDER BY list_label
327 LIMIT 25"""],
328 context = ctxt
329 )
330 mp.setThresholds(1, 3, 5)
331 mp.unset_context('pat')
332
333 self.matcher = mp
334 self.picklist_delay = 50
335 self.selection_only = True
336
337 self.SetToolTip(_('Select a document.'))
338
339 #--------------------------------------------------------
341 if len(self._data) == 0:
342 return None
343 return gmDocuments.cDocument(aPK_obj = self.GetData())
344
345 #--------------------------------------------------------
347 if len(self._data) == 0:
348 return ''
349 return gmDocuments.cDocument(aPK_obj = self.GetData()).format(single_line = False)
350
351 #============================================================
353 """Let user select a document comment from all existing comments."""
355
356 gmPhraseWheel.cPhraseWheel.__init__(self, *args, **kwargs)
357
358 context = {
359 'ctxt_doc_type': {
360 'where_part': 'and fk_type = %(pk_doc_type)s',
361 'placeholder': 'pk_doc_type'
362 }
363 }
364
365 mp = gmMatchProvider.cMatchProvider_SQL2 (
366 queries = ["""
367 SELECT
368 data,
369 field_label,
370 list_label
371 FROM (
372 SELECT DISTINCT ON (field_label) *
373 FROM (
374 -- constrained by doc type
375 SELECT
376 comment AS data,
377 comment AS field_label,
378 comment AS list_label,
379 1 AS rank
380 FROM blobs.doc_med
381 WHERE
382 comment %(fragment_condition)s
383 %(ctxt_doc_type)s
384
385 UNION ALL
386
387 SELECT
388 comment AS data,
389 comment AS field_label,
390 comment AS list_label,
391 2 AS rank
392 FROM blobs.doc_med
393 WHERE
394 comment %(fragment_condition)s
395 ) AS q_union
396 ) AS q_distinct
397 ORDER BY rank, list_label
398 LIMIT 25"""],
399 context = context
400 )
401 mp.setThresholds(3, 5, 7)
402 mp.unset_context('pk_doc_type')
403
404 self.matcher = mp
405 self.picklist_delay = 50
406
407 self.SetToolTip(_('Enter a comment on the document.'))
408
409 #============================================================
410 # document type widgets
411 #============================================================
413
414 if parent is None:
415 parent = wx.GetApp().GetTopWindow()
416
417 dlg = cEditDocumentTypesDlg(parent = parent)
418 dlg.ShowModal()
419
420 #============================================================
421 from Gnumed.wxGladeWidgets import wxgEditDocumentTypesDlg
422
428
429 #============================================================
430 from Gnumed.wxGladeWidgets import wxgEditDocumentTypesPnl
431
433 """A panel grouping together fields to edit the list of document types."""
434
436 wxgEditDocumentTypesPnl.wxgEditDocumentTypesPnl.__init__(self, *args, **kwargs)
437 self.__init_ui()
438 self.__register_interests()
439 self.repopulate_ui()
440 #--------------------------------------------------------
442 self._LCTRL_doc_type.set_columns([_('Type'), _('Translation'), _('User defined'), _('In use')])
443 self._LCTRL_doc_type.set_column_widths()
444 #--------------------------------------------------------
447 #--------------------------------------------------------
449 self.repopulate_ui()
450 #--------------------------------------------------------
452
453 self._LCTRL_doc_type.DeleteAllItems()
454
455 doc_types = gmDocuments.get_document_types()
456 pos = len(doc_types) + 1
457
458 for doc_type in doc_types:
459 row_num = self._LCTRL_doc_type.InsertItem(pos, label = doc_type['type'])
460 self._LCTRL_doc_type.SetItem(index = row_num, column = 1, label = doc_type['l10n_type'])
461 if doc_type['is_user_defined']:
462 self._LCTRL_doc_type.SetItem(index = row_num, column = 2, label = ' X ')
463 if doc_type['is_in_use']:
464 self._LCTRL_doc_type.SetItem(index = row_num, column = 3, label = ' X ')
465
466 if len(doc_types) > 0:
467 self._LCTRL_doc_type.set_data(data = doc_types)
468 self._LCTRL_doc_type.SetColumnWidth(0, wx.LIST_AUTOSIZE)
469 self._LCTRL_doc_type.SetColumnWidth(1, wx.LIST_AUTOSIZE)
470 self._LCTRL_doc_type.SetColumnWidth(2, wx.LIST_AUTOSIZE_USEHEADER)
471 self._LCTRL_doc_type.SetColumnWidth(3, wx.LIST_AUTOSIZE_USEHEADER)
472
473 self._TCTRL_type.SetValue('')
474 self._TCTRL_l10n_type.SetValue('')
475
476 self._BTN_set_translation.Enable(False)
477 self._BTN_delete.Enable(False)
478 self._BTN_add.Enable(False)
479 self._BTN_reassign.Enable(False)
480
481 self._LCTRL_doc_type.SetFocus()
482 #--------------------------------------------------------
483 # event handlers
484 #--------------------------------------------------------
486 doc_type = self._LCTRL_doc_type.get_selected_item_data()
487
488 self._TCTRL_type.SetValue(doc_type['type'])
489 self._TCTRL_l10n_type.SetValue(doc_type['l10n_type'])
490
491 self._BTN_set_translation.Enable(True)
492 self._BTN_delete.Enable(not bool(doc_type['is_in_use']))
493 self._BTN_add.Enable(False)
494 self._BTN_reassign.Enable(True)
495
496 return
497 #--------------------------------------------------------
499 self._BTN_set_translation.Enable(False)
500 self._BTN_delete.Enable(False)
501 self._BTN_reassign.Enable(False)
502
503 self._BTN_add.Enable(True)
504 # self._LCTRL_doc_type.deselect_selected_item()
505 return
506 #--------------------------------------------------------
513 #--------------------------------------------------------
530 #--------------------------------------------------------
540 #--------------------------------------------------------
572
573 #============================================================
575 """Let user select a document type."""
577
578 gmPhraseWheel.cPhraseWheel.__init__(self, *args, **kwargs)
579
580 mp = gmMatchProvider.cMatchProvider_SQL2 (
581 queries = [
582 """SELECT
583 data,
584 field_label,
585 list_label
586 FROM ((
587 SELECT
588 pk_doc_type AS data,
589 l10n_type AS field_label,
590 l10n_type AS list_label,
591 1 AS rank
592 FROM blobs.v_doc_type
593 WHERE
594 is_user_defined IS True
595 AND
596 l10n_type %(fragment_condition)s
597 ) UNION (
598 SELECT
599 pk_doc_type AS data,
600 l10n_type AS field_label,
601 l10n_type AS list_label,
602 2 AS rank
603 FROM blobs.v_doc_type
604 WHERE
605 is_user_defined IS False
606 AND
607 l10n_type %(fragment_condition)s
608 )) AS q1
609 ORDER BY q1.rank, q1.list_label"""]
610 )
611 mp.setThresholds(2, 4, 6)
612
613 self.matcher = mp
614 self.picklist_delay = 50
615
616 self.SetToolTip(_('Select the document type.'))
617 #--------------------------------------------------------
619
620 doc_type = self.GetValue().strip()
621 if doc_type == '':
622 gmDispatcher.send(signal = 'statustext', msg = _('Cannot create document type without name.'), beep = True)
623 _log.debug('cannot create document type without name')
624 return
625
626 pk = gmDocuments.create_document_type(doc_type)['pk_doc_type']
627 if pk is None:
628 self.data = {}
629 else:
630 self.SetText (
631 value = doc_type,
632 data = pk
633 )
634
635 #============================================================
636 # document review widgets
637 #============================================================
639 if parent is None:
640 parent = wx.GetApp().GetTopWindow()
641 dlg = cReviewDocPartDlg (
642 parent = parent,
643 id = -1,
644 part = part
645 )
646 dlg.ShowModal()
647 dlg.Destroy()
648
649 #------------------------------------------------------------
652
653 #------------------------------------------------------------
654 from Gnumed.wxGladeWidgets import wxgReviewDocPartDlg
655
658 """Support parts and docs now.
659 """
660 part = kwds['part']
661 del kwds['part']
662 wxgReviewDocPartDlg.wxgReviewDocPartDlg.__init__(self, *args, **kwds)
663
664 if isinstance(part, gmDocuments.cDocumentPart):
665 self.__part = part
666 self.__doc = self.__part.get_containing_document()
667 self.__reviewing_doc = False
668 elif isinstance(part, gmDocuments.cDocument):
669 self.__doc = part
670 if len(self.__doc.parts) == 0:
671 self.__part = None
672 else:
673 self.__part = self.__doc.parts[0]
674 self.__reviewing_doc = True
675 else:
676 raise ValueError('<part> must be gmDocuments.cDocument or gmDocuments.cDocumentPart instance, got <%s>' % type(part))
677
678 self.__init_ui_data()
679
680 #--------------------------------------------------------
681 # internal API
682 #--------------------------------------------------------
684 # FIXME: fix this
685 # associated episode (add " " to avoid popping up pick list)
686 self._PhWheel_episode.SetText('%s ' % self.__doc['episode'], self.__doc['pk_episode'])
687 self._PhWheel_doc_type.SetText(value = self.__doc['l10n_type'], data = self.__doc['pk_type'])
688 self._PhWheel_doc_type.add_callback_on_set_focus(self._on_doc_type_gets_focus)
689 self._PhWheel_doc_type.add_callback_on_lose_focus(self._on_doc_type_loses_focus)
690
691 if self.__reviewing_doc:
692 self._PRW_doc_comment.SetText(gmTools.coalesce(self.__doc['comment'], ''))
693 self._PRW_doc_comment.set_context(context = 'pk_doc_type', val = self.__doc['pk_type'])
694 else:
695 self._PRW_doc_comment.SetText(gmTools.coalesce(self.__part['obj_comment'], ''))
696
697 if self.__doc['pk_org_unit'] is not None:
698 self._PRW_org.SetText(value = '%s @ %s' % (self.__doc['unit'], self.__doc['organization']), data = self.__doc['pk_org_unit'])
699
700 if self.__doc['unit_is_receiver']:
701 self._RBTN_org_is_receiver.Value = True
702 else:
703 self._RBTN_org_is_source.Value = True
704
705 if self.__reviewing_doc:
706 self._PRW_org.Enable()
707 else:
708 self._PRW_org.Disable()
709
710 if self.__doc['pk_hospital_stay'] is not None:
711 self._PRW_hospital_stay.SetText(data = self.__doc['pk_hospital_stay'])
712
713 fts = gmDateTime.cFuzzyTimestamp(timestamp = self.__doc['clin_when'])
714 self._PhWheel_doc_date.SetText(fts.strftime('%Y-%m-%d'), fts)
715 self._TCTRL_reference.SetValue(gmTools.coalesce(self.__doc['ext_ref'], ''))
716 if self.__reviewing_doc:
717 self._TCTRL_filename.Enable(False)
718 self._SPINCTRL_seq_idx.Enable(False)
719 else:
720 self._TCTRL_filename.SetValue(gmTools.coalesce(self.__part['filename'], ''))
721 self._SPINCTRL_seq_idx.SetValue(gmTools.coalesce(self.__part['seq_idx'], 0))
722
723 self._LCTRL_existing_reviews.InsertColumn(0, _('who'))
724 self._LCTRL_existing_reviews.InsertColumn(1, _('when'))
725 self._LCTRL_existing_reviews.InsertColumn(2, _('+/-'))
726 self._LCTRL_existing_reviews.InsertColumn(3, _('!'))
727 self._LCTRL_existing_reviews.InsertColumn(4, _('comment'))
728
729 self.__reload_existing_reviews()
730
731 if self._LCTRL_existing_reviews.GetItemCount() > 0:
732 self._LCTRL_existing_reviews.SetColumnWidth(0, wx.LIST_AUTOSIZE)
733 self._LCTRL_existing_reviews.SetColumnWidth(1, wx.LIST_AUTOSIZE)
734 self._LCTRL_existing_reviews.SetColumnWidth(2, wx.LIST_AUTOSIZE_USEHEADER)
735 self._LCTRL_existing_reviews.SetColumnWidth(3, wx.LIST_AUTOSIZE_USEHEADER)
736 self._LCTRL_existing_reviews.SetColumnWidth(4, wx.LIST_AUTOSIZE)
737
738 if self.__part is None:
739 self._ChBOX_review.SetValue(False)
740 self._ChBOX_review.Enable(False)
741 self._ChBOX_abnormal.Enable(False)
742 self._ChBOX_relevant.Enable(False)
743 self._ChBOX_sign_all_pages.Enable(False)
744 else:
745 me = gmStaff.gmCurrentProvider()
746 if self.__part['pk_intended_reviewer'] == me['pk_staff']:
747 msg = _('(you are the primary reviewer)')
748 else:
749 other = gmStaff.cStaff(aPK_obj = self.__part['pk_intended_reviewer'])
750 msg = _('(someone else is the intended reviewer: %s)') % other['short_alias']
751 self._TCTRL_responsible.SetValue(msg)
752 # init my review if any
753 if self.__part['reviewed_by_you']:
754 revs = self.__part.get_reviews()
755 for rev in revs:
756 if rev['is_your_review']:
757 self._ChBOX_abnormal.SetValue(bool(rev[2]))
758 self._ChBOX_relevant.SetValue(bool(rev[3]))
759 break
760
761 self._ChBOX_sign_all_pages.SetValue(self.__reviewing_doc)
762
763 return True
764
765 #--------------------------------------------------------
767 self._LCTRL_existing_reviews.DeleteAllItems()
768 if self.__part is None:
769 return True
770 revs = self.__part.get_reviews() # FIXME: this is ugly as sin, it should be dicts, not lists
771 if len(revs) == 0:
772 return True
773 # find special reviews
774 review_by_responsible_doc = None
775 reviews_by_others = []
776 for rev in revs:
777 if rev['is_review_by_responsible_reviewer'] and not rev['is_your_review']:
778 review_by_responsible_doc = rev
779 if not (rev['is_review_by_responsible_reviewer'] or rev['is_your_review']):
780 reviews_by_others.append(rev)
781 # display them
782 if review_by_responsible_doc is not None:
783 row_num = self._LCTRL_existing_reviews.InsertItem(sys.maxsize, label=review_by_responsible_doc[0])
784 self._LCTRL_existing_reviews.SetItemTextColour(row_num, column=wx.BLUE)
785 self._LCTRL_existing_reviews.SetItem(index = row_num, column=0, label=review_by_responsible_doc[0])
786 self._LCTRL_existing_reviews.SetItem(index = row_num, column=1, label=review_by_responsible_doc[1].strftime('%x %H:%M'))
787 if review_by_responsible_doc['is_technically_abnormal']:
788 self._LCTRL_existing_reviews.SetItem(index = row_num, column=2, label='X')
789 if review_by_responsible_doc['clinically_relevant']:
790 self._LCTRL_existing_reviews.SetItem(index = row_num, column=3, label='X')
791 self._LCTRL_existing_reviews.SetItem(index = row_num, column=4, label=review_by_responsible_doc[6])
792 row_num += 1
793 for rev in reviews_by_others:
794 row_num = self._LCTRL_existing_reviews.InsertItem(sys.maxsize, label=rev[0])
795 self._LCTRL_existing_reviews.SetItem(index = row_num, column=0, label=rev[0])
796 self._LCTRL_existing_reviews.SetItem(index = row_num, column=1, label=rev[1].strftime('%x %H:%M'))
797 if rev['is_technically_abnormal']:
798 self._LCTRL_existing_reviews.SetItem(index = row_num, column=2, label='X')
799 if rev['clinically_relevant']:
800 self._LCTRL_existing_reviews.SetItem(index = row_num, column=3, label='X')
801 self._LCTRL_existing_reviews.SetItem(index = row_num, column=4, label=rev[6])
802 return True
803
804 #--------------------------------------------------------
805 # event handlers
806 #--------------------------------------------------------
903
904 #--------------------------------------------------------
906 state = self._ChBOX_review.GetValue()
907 self._ChBOX_abnormal.Enable(enable = state)
908 self._ChBOX_relevant.Enable(enable = state)
909 self._ChBOX_responsible.Enable(enable = state)
910
911 #--------------------------------------------------------
913 """Per Jim: Changing the doc type happens a lot more often
914 then correcting spelling, hence select-all on getting focus.
915 """
916 self._PhWheel_doc_type.SetSelection(-1, -1)
917
918 #--------------------------------------------------------
920 pk_doc_type = self._PhWheel_doc_type.GetData()
921 if pk_doc_type is None:
922 self._PRW_doc_comment.unset_context(context = 'pk_doc_type')
923 else:
924 self._PRW_doc_comment.set_context(context = 'pk_doc_type', val = pk_doc_type)
925 return True
926
927 #============================================================
929
930 _log.debug('acquiring images from [%s]', device)
931
932 # do not import globally since we might want to use
933 # this module without requiring any scanner to be available
934 from Gnumed.pycommon import gmScanBackend
935 try:
936 fnames = gmScanBackend.acquire_pages_into_files (
937 device = device,
938 delay = 5,
939 calling_window = calling_window
940 )
941 except OSError:
942 _log.exception('problem acquiring image from source')
943 gmGuiHelpers.gm_show_error (
944 aMessage = _(
945 'No images could be acquired from the source.\n\n'
946 'This may mean the scanner driver is not properly installed.\n\n'
947 'On Windows you must install the TWAIN Python module\n'
948 'while on Linux and MacOSX it is recommended to install\n'
949 'the XSane package.'
950 ),
951 aTitle = _('Acquiring images')
952 )
953 return None
954
955 _log.debug('acquired %s images', len(fnames))
956
957 return fnames
958
959 #------------------------------------------------------------
960 from Gnumed.wxGladeWidgets import wxgScanIdxPnl
961
963
965 wxgScanIdxPnl.wxgScanIdxPnl.__init__(self, *args, **kwds)
966 gmPlugin.cPatientChange_PluginMixin.__init__(self)
967
968 self._PhWheel_reviewer.matcher = gmPerson.cMatchProvider_Provider()
969
970 self.__init_ui_data()
971 self._PhWheel_doc_type.add_callback_on_lose_focus(self._on_doc_type_loses_focus)
972
973 # make me and listctrl file drop targets
974 dt = gmGuiHelpers.cFileDropTarget(target = self)
975 self.SetDropTarget(dt)
976 dt = gmGuiHelpers.cFileDropTarget(on_drop_callback = self._drop_target_consume_filenames)
977 self._LCTRL_doc_pages.SetDropTarget(dt)
978
979 # do not import globally since we might want to use
980 # this module without requiring any scanner to be available
981 from Gnumed.pycommon import gmScanBackend
982 self.scan_module = gmScanBackend
983
984 #--------------------------------------------------------
985 # file drop target API
986 #--------------------------------------------------------
988 pat = gmPerson.gmCurrentPatient()
989 if not pat.connected:
990 gmDispatcher.send(signal='statustext', msg=_('Cannot accept new documents. No active patient.'))
991 return
992
993 # dive into folders dropped onto us and extract files (one level deep only)
994 real_filenames = []
995 for pathname in filenames:
996 try:
997 files = os.listdir(pathname)
998 source = _('directory dropped on client')
999 gmDispatcher.send(signal = 'statustext', msg = _('Extracting files from folder [%s] ...') % pathname)
1000 for filename in files:
1001 fullname = os.path.join(pathname, filename)
1002 if not os.path.isfile(fullname):
1003 continue
1004 real_filenames.append(fullname)
1005 except OSError:
1006 source = _('file dropped on client')
1007 real_filenames.append(pathname)
1008
1009 self.add_parts_from_files(real_filenames, source)
1010
1011 #--------------------------------------------------------
1014
1015 #--------------------------------------------------------
1016 # patient change plugin API
1017 #--------------------------------------------------------
1021
1022 #--------------------------------------------------------
1025
1026 #--------------------------------------------------------
1027 # internal API
1028 #--------------------------------------------------------
1030 # -----------------------------
1031 self._PhWheel_episode.SetText(value = _('other documents'), suppress_smarts = True)
1032 self._PhWheel_doc_type.SetText('')
1033 # -----------------------------
1034 # FIXME: make this configurable: either now() or last_date()
1035 fts = gmDateTime.cFuzzyTimestamp()
1036 self._PhWheel_doc_date.SetText(fts.strftime('%Y-%m-%d'), fts)
1037 self._PRW_doc_comment.SetText('')
1038 self._PhWheel_source.SetText('', None)
1039 self._RBTN_org_is_source.SetValue(1)
1040 # FIXME: should be set to patient's primary doc
1041 self._PhWheel_reviewer.selection_only = True
1042 me = gmStaff.gmCurrentProvider()
1043 self._PhWheel_reviewer.SetText (
1044 value = '%s (%s%s %s)' % (me['short_alias'], gmTools.coalesce(me['title'], ''), me['firstnames'], me['lastnames']),
1045 data = me['pk_staff']
1046 )
1047 # -----------------------------
1048 # FIXME: set from config item
1049 self._ChBOX_reviewed.SetValue(False)
1050 self._ChBOX_abnormal.Disable()
1051 self._ChBOX_abnormal.SetValue(False)
1052 self._ChBOX_relevant.Disable()
1053 self._ChBOX_relevant.SetValue(False)
1054 # -----------------------------
1055 self._TBOX_description.SetValue('')
1056 # -----------------------------
1057 # the list holding our page files
1058 self._LCTRL_doc_pages.remove_items_safely()
1059 self._LCTRL_doc_pages.set_columns([_('file'), _('path')])
1060 self._LCTRL_doc_pages.set_column_widths()
1061
1062 self._TCTRL_metadata.SetValue('')
1063
1064 self._PhWheel_doc_type.SetFocus()
1065
1066 #--------------------------------------------------------
1068 rows = gmTools.coalesce(self._LCTRL_doc_pages.string_items, [])
1069 data = gmTools.coalesce(self._LCTRL_doc_pages.data, [])
1070 rows.extend([ [gmTools.fname_from_path(f), gmTools.fname_dir(f)] for f in filenames ])
1071 data.extend([ [f, source] for f in filenames ])
1072 self._LCTRL_doc_pages.string_items = rows
1073 self._LCTRL_doc_pages.data = data
1074 self._LCTRL_doc_pages.set_column_widths()
1075
1076 #--------------------------------------------------------
1078 title = _('saving document')
1079
1080 if self._LCTRL_doc_pages.ItemCount == 0:
1081 dbcfg = gmCfg.cCfgSQL()
1082 allow_empty = bool(dbcfg.get2 (
1083 option = 'horstspace.scan_index.allow_partless_documents',
1084 workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace,
1085 bias = 'user',
1086 default = False
1087 ))
1088 if allow_empty:
1089 save_empty = gmGuiHelpers.gm_show_question (
1090 aMessage = _('No parts to save. Really save an empty document as a reference ?'),
1091 aTitle = title
1092 )
1093 if not save_empty:
1094 return False
1095 else:
1096 gmGuiHelpers.gm_show_error (
1097 aMessage = _('No parts to save. Aquire some parts first.'),
1098 aTitle = title
1099 )
1100 return False
1101
1102 doc_type_pk = self._PhWheel_doc_type.GetData(can_create = True)
1103 if doc_type_pk is None:
1104 gmGuiHelpers.gm_show_error (
1105 aMessage = _('No document type applied. Choose a document type'),
1106 aTitle = title
1107 )
1108 return False
1109
1110 # this should be optional, actually
1111 # if self._PRW_doc_comment.GetValue().strip() == '':
1112 # gmGuiHelpers.gm_show_error (
1113 # aMessage = _('No document comment supplied. Add a comment for this document.'),
1114 # aTitle = title
1115 # )
1116 # return False
1117
1118 if self._PhWheel_episode.GetValue().strip() == '':
1119 gmGuiHelpers.gm_show_error (
1120 aMessage = _('You must select an episode to save this document under.'),
1121 aTitle = title
1122 )
1123 return False
1124
1125 if self._PhWheel_reviewer.GetData() is None:
1126 gmGuiHelpers.gm_show_error (
1127 aMessage = _('You need to select from the list of staff members the doctor who is intended to sign the document.'),
1128 aTitle = title
1129 )
1130 return False
1131
1132 if self._PhWheel_doc_date.is_valid_timestamp(empty_is_valid = True) is False:
1133 gmGuiHelpers.gm_show_error (
1134 aMessage = _('Invalid date of generation.'),
1135 aTitle = title
1136 )
1137 return False
1138
1139 return True
1140
1141 #--------------------------------------------------------
1143
1144 if not reconfigure:
1145 dbcfg = gmCfg.cCfgSQL()
1146 device = dbcfg.get2 (
1147 option = 'external.xsane.default_device',
1148 workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace,
1149 bias = 'workplace',
1150 default = ''
1151 )
1152 if device.strip() == '':
1153 device = None
1154 if device is not None:
1155 return device
1156
1157 try:
1158 devices = self.scan_module.get_devices()
1159 except:
1160 _log.exception('cannot retrieve list of image sources')
1161 gmDispatcher.send(signal = 'statustext', msg = _('There is no scanner support installed on this machine.'))
1162 return None
1163
1164 if devices is None:
1165 # get_devices() not implemented for TWAIN yet
1166 # XSane has its own chooser (so does TWAIN)
1167 return None
1168
1169 if len(devices) == 0:
1170 gmDispatcher.send(signal = 'statustext', msg = _('Cannot find an active scanner.'))
1171 return None
1172
1173 # device_names = []
1174 # for device in devices:
1175 # device_names.append('%s (%s)' % (device[2], device[0]))
1176
1177 device = gmListWidgets.get_choices_from_list (
1178 parent = self,
1179 msg = _('Select an image capture device'),
1180 caption = _('device selection'),
1181 choices = [ '%s (%s)' % (d[2], d[0]) for d in devices ],
1182 columns = [_('Device')],
1183 data = devices,
1184 single_selection = True
1185 )
1186 if device is None:
1187 return None
1188
1189 # FIXME: add support for actually reconfiguring
1190 return device[0]
1191
1192 #--------------------------------------------------------
1193 # event handling API
1194 #--------------------------------------------------------
1196
1197 chosen_device = self.get_device_to_use()
1198
1199 # FIXME: configure whether to use XSane or sane directly
1200 # FIXME: add support for xsane_device_settings argument
1201 try:
1202 fnames = self.scan_module.acquire_pages_into_files (
1203 device = chosen_device,
1204 delay = 5,
1205 calling_window = self
1206 )
1207 except OSError:
1208 _log.exception('problem acquiring image from source')
1209 gmGuiHelpers.gm_show_error (
1210 aMessage = _(
1211 'No pages could be acquired from the source.\n\n'
1212 'This may mean the scanner driver is not properly installed.\n\n'
1213 'On Windows you must install the TWAIN Python module\n'
1214 'while on Linux and MacOSX it is recommended to install\n'
1215 'the XSane package.'
1216 ),
1217 aTitle = _('acquiring page')
1218 )
1219 return None
1220
1221 if len(fnames) == 0: # no pages scanned
1222 return True
1223
1224 self.add_parts_from_files(fnames, _('captured by imaging device'))
1225 return True
1226
1227 #--------------------------------------------------------
1229 # patient file chooser
1230 dlg = wx.FileDialog (
1231 parent = None,
1232 message = _('Choose a file'),
1233 defaultDir = os.path.expanduser(os.path.join('~', 'gnumed')),
1234 defaultFile = '',
1235 wildcard = "%s (*)|*|TIFFs (*.tif)|*.tif|JPEGs (*.jpg)|*.jpg|%s (*.*)|*.*" % (_('all files'), _('all files (Win)')),
1236 style = wx.FD_OPEN | wx.FD_FILE_MUST_EXIST | wx.FD_MULTIPLE
1237 )
1238 result = dlg.ShowModal()
1239 files = dlg.GetPaths()
1240 if result == wx.ID_CANCEL:
1241 dlg.Destroy()
1242 return
1243
1244 self.add_parts_from_files(files, _('picked from storage media'))
1245
1246 #--------------------------------------------------------
1248 event.Skip()
1249 clip = gmGuiHelpers.clipboard2file()
1250 if clip is None:
1251 return
1252 if clip is False:
1253 return
1254 self.add_parts_from_files([clip], _('pasted from clipboard'))
1255
1256 #--------------------------------------------------------
1258
1259 # nothing to do
1260 if self._LCTRL_doc_pages.ItemCount == 0:
1261 return
1262
1263 # only one page, show that, regardless of whether selected or not
1264 if self._LCTRL_doc_pages.ItemCount == 1:
1265 page_fnames = [ self._LCTRL_doc_pages.get_item_data(0)[0] ]
1266 else:
1267 # did user select one of multiple pages ?
1268 page_fnames = [ data[0] for data in self._LCTRL_doc_pages.selected_item_data ]
1269 if len(page_fnames) == 0:
1270 gmDispatcher.send(signal = 'statustext', msg = _('No part selected for viewing.'), beep = True)
1271 return
1272
1273 for page_fname in page_fnames:
1274 (success, msg) = gmMimeLib.call_viewer_on_file(page_fname)
1275 if not success:
1276 gmGuiHelpers.gm_show_warning (
1277 aMessage = _('Cannot display document part:\n%s') % msg,
1278 aTitle = _('displaying part')
1279 )
1280
1281 #--------------------------------------------------------
1283
1284 if len(self._LCTRL_doc_pages.selected_items) == 0:
1285 gmDispatcher.send(signal = 'statustext', msg = _('No part selected for removal.'), beep = True)
1286 return
1287
1288 sel_idx = self._LCTRL_doc_pages.GetFirstSelected()
1289 rows = self._LCTRL_doc_pages.string_items
1290 data = self._LCTRL_doc_pages.data
1291 del rows[sel_idx]
1292 del data[sel_idx]
1293 self._LCTRL_doc_pages.string_items = rows
1294 self._LCTRL_doc_pages.data = data
1295 self._LCTRL_doc_pages.set_column_widths()
1296 self._TCTRL_metadata.SetValue('')
1297
1298 #--------------------------------------------------------
1300
1301 if not self.__valid_for_save():
1302 return False
1303
1304 # external reference
1305 cfg = gmCfg.cCfgSQL()
1306 generate_uuid = bool (
1307 cfg.get2 (
1308 option = 'horstspace.scan_index.generate_doc_uuid',
1309 workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace,
1310 bias = 'user',
1311 default = False
1312 )
1313 )
1314 if generate_uuid:
1315 ext_ref = gmDocuments.get_ext_ref()
1316 else:
1317 ext_ref = None
1318
1319 # create document
1320 new_doc = save_files_as_new_document (
1321 parent = self,
1322 filenames = [ data[0] for data in self._LCTRL_doc_pages.data ],
1323 document_type = self._PhWheel_doc_type.GetValue().strip(),
1324 pk_document_type = self._PhWheel_doc_type.GetData(),
1325 unlock_patient = False,
1326 episode = self._PhWheel_episode.GetData(can_create = True, is_open = True, as_instance = True),
1327 review_as_normal = False,
1328 reference = ext_ref,
1329 pk_org_unit = self._PhWheel_source.GetData(),
1330 # date_generated = self._PhWheel_doc_date.GetData().get_pydt(),
1331 date_generated = gmTools.coalesce (
1332 self._PhWheel_doc_date.GetData(),
1333 function_initial = 'get_pydt'
1334 ),
1335 comment = self._PRW_doc_comment.GetLineText(0).strip(),
1336 reviewer = self._PhWheel_reviewer.GetData()
1337 )
1338 if new_doc is None:
1339 return False
1340
1341 if self._RBTN_org_is_receiver.Value is True:
1342 new_doc['unit_is_receiver'] = True
1343 new_doc.save()
1344
1345 # - long description
1346 description = self._TBOX_description.GetValue().strip()
1347 if description != '':
1348 if not new_doc.add_description(description):
1349 wx.EndBusyCursor()
1350 gmGuiHelpers.gm_show_error (
1351 aMessage = _('Cannot add document description.'),
1352 aTitle = _('saving document')
1353 )
1354 return False
1355
1356 # set reviewed status
1357 if self._ChBOX_reviewed.GetValue():
1358 if not new_doc.set_reviewed (
1359 technically_abnormal = self._ChBOX_abnormal.GetValue(),
1360 clinically_relevant = self._ChBOX_relevant.GetValue()
1361 ):
1362 msg = _('Error setting "reviewed" status of new document.')
1363
1364 self.__init_ui_data()
1365
1366 gmHooks.run_hook_script(hook = 'after_new_doc_created')
1367
1368 return True
1369
1370 #--------------------------------------------------------
1373
1374 #--------------------------------------------------------
1376 self._ChBOX_abnormal.Enable(enable = self._ChBOX_reviewed.GetValue())
1377 self._ChBOX_relevant.Enable(enable = self._ChBOX_reviewed.GetValue())
1378
1379 #--------------------------------------------------------
1381 pk_doc_type = self._PhWheel_doc_type.GetData()
1382 if pk_doc_type is None:
1383 self._PRW_doc_comment.unset_context(context = 'pk_doc_type')
1384 else:
1385 self._PRW_doc_comment.set_context(context = 'pk_doc_type', val = pk_doc_type)
1386 return True
1387
1388 #--------------------------------------------------------
1390 status, description = result
1391 fname, source = self._LCTRL_doc_pages.get_selected_item_data(only_one = True)
1392 txt = _(
1393 'Source: %s\n'
1394 'File: %s\n'
1395 '\n'
1396 '%s'
1397 ) % (
1398 source,
1399 fname,
1400 description
1401 )
1402 wx.CallAfter(self._TCTRL_metadata.SetValue, txt)
1403
1404 #--------------------------------------------------------
1406 event.Skip()
1407 fname, source = self._LCTRL_doc_pages.get_item_data(item_idx = event.Index)
1408 self._TCTRL_metadata.SetValue('Retrieving details from [%s] ...' % fname)
1409 gmMimeLib.describe_file(fname, callback = self._on_update_file_description)
1410
1411 #============================================================
1413
1414 if parent is None:
1415 parent = wx.GetApp().GetTopWindow()
1416
1417 # sanity check
1418 if part['size'] == 0:
1419 _log.debug('cannot display part [%s] - 0 bytes', part['pk_obj'])
1420 gmGuiHelpers.gm_show_error (
1421 aMessage = _('Document part does not seem to exist in database !'),
1422 aTitle = _('showing document')
1423 )
1424 return None
1425
1426 wx.BeginBusyCursor()
1427 cfg = gmCfg.cCfgSQL()
1428
1429 # determine database export chunk size
1430 chunksize = int(
1431 cfg.get2 (
1432 option = "horstspace.blob_export_chunk_size",
1433 workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace,
1434 bias = 'workplace',
1435 default = 2048
1436 ))
1437
1438 # shall we force blocking during view ?
1439 block_during_view = bool( cfg.get2 (
1440 option = 'horstspace.document_viewer.block_during_view',
1441 workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace,
1442 bias = 'user',
1443 default = None
1444 ))
1445
1446 wx.EndBusyCursor()
1447
1448 # display it
1449 successful, msg = part.display_via_mime (
1450 chunksize = chunksize,
1451 block = block_during_view
1452 )
1453 if not successful:
1454 gmGuiHelpers.gm_show_error (
1455 aMessage = _('Cannot display document part:\n%s') % msg,
1456 aTitle = _('showing document')
1457 )
1458 return None
1459
1460 # handle review after display
1461 # 0: never
1462 # 1: always
1463 # 2: if no review by myself exists yet
1464 # 3: if no review at all exists yet
1465 # 4: if no review by responsible reviewer
1466 review_after_display = int(cfg.get2 (
1467 option = 'horstspace.document_viewer.review_after_display',
1468 workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace,
1469 bias = 'user',
1470 default = 3
1471 ))
1472 if review_after_display == 1: # always review
1473 review_document_part(parent = parent, part = part)
1474 elif review_after_display == 2: # review if no review by me exists
1475 review_by_me = [ rev for rev in part.get_reviews() if rev['is_your_review'] ]
1476 if len(review_by_me) == 0:
1477 review_document_part(parent = parent, part = part)
1478 elif review_after_display == 3:
1479 if len(part.get_reviews()) == 0:
1480 review_document_part(parent = parent, part = part)
1481 elif review_after_display == 4:
1482 reviewed_by_responsible = [ rev for rev in part.get_reviews() if rev['is_review_by_responsible_reviewer'] ]
1483 if len(reviewed_by_responsible) == 0:
1484 review_document_part(parent = parent, part = part)
1485
1486 return True
1487
1488 #============================================================
1489 -def manage_documents(parent=None, msg=None, single_selection=True, pk_types=None, pk_episodes=None):
1490
1491 pat = gmPerson.gmCurrentPatient()
1492
1493 if parent is None:
1494 parent = wx.GetApp().GetTopWindow()
1495
1496 #--------------------------------------------------------
1497 def edit(document=None):
1498 return
1499 #return edit_substance(parent = parent, substance = substance, single_entry = (substance is not None))
1500
1501 #--------------------------------------------------------
1502 def delete(document):
1503 return
1504 # if substance.is_in_use_by_patients:
1505 # gmDispatcher.send(signal = 'statustext', msg = _('Cannot delete this substance. It is in use.'), beep = True)
1506 # return False
1507 #
1508 # return gmMedication.delete_x_substance(substance = substance['pk'])
1509
1510 #------------------------------------------------------------
1511 def refresh(lctrl):
1512 docs = pat.document_folder.get_documents(pk_types = pk_types, pk_episodes = pk_episodes)
1513 items = [ [
1514 gmDateTime.pydt_strftime(d['clin_when'], '%Y %b %d', accuracy = gmDateTime.acc_days),
1515 d['l10n_type'],
1516 gmTools.coalesce(d['comment'], ''),
1517 gmTools.coalesce(d['ext_ref'], ''),
1518 d['pk_doc']
1519 ] for d in docs ]
1520 lctrl.set_string_items(items)
1521 lctrl.set_data(docs)
1522
1523 #--------------------------------------------------------
1524 def show_doc(doc):
1525 if doc is None:
1526 return
1527 for fname in doc.save_parts_to_files():
1528 gmMimeLib.call_viewer_on_file(aFile = fname, block = False)
1529
1530 #------------------------------------------------------------
1531 return gmListWidgets.get_choices_from_list (
1532 parent = parent,
1533 caption = _('Patient document list'),
1534 columns = [_('Generated'), _('Type'), _('Comment'), _('Ref #'), '#'],
1535 single_selection = single_selection,
1536 #new_callback = edit,
1537 #edit_callback = edit,
1538 #delete_callback = delete,
1539 refresh_callback = refresh,
1540 left_extra_button = (_('Show'), _('Show all parts of this document in external viewer.'), show_doc)
1541 )
1542
1543 #============================================================
1544 from Gnumed.wxGladeWidgets import wxgSelectablySortedDocTreePnl
1545
1546 -class cSelectablySortedDocTreePnl(wxgSelectablySortedDocTreePnl.wxgSelectablySortedDocTreePnl):
1547 """A panel with a document tree which can be sorted."""
1548
1550 wxgSelectablySortedDocTreePnl.wxgSelectablySortedDocTreePnl.__init__(self, parent, id, *args, **kwds)
1551
1552 self._LCTRL_details.set_columns(['', ''])
1553
1554 self._doc_tree.show_details_callback = self._update_details
1555
1556 #--------------------------------------------------------
1557 # inherited event handlers
1558 #--------------------------------------------------------
1560 self._doc_tree.sort_mode = 'age'
1561 self._doc_tree.SetFocus()
1562 self._rbtn_sort_by_age.SetValue(True)
1563
1564 #--------------------------------------------------------
1566 self._doc_tree.sort_mode = 'review'
1567 self._doc_tree.SetFocus()
1568 self._rbtn_sort_by_review.SetValue(True)
1569
1570 #--------------------------------------------------------
1572 self._doc_tree.sort_mode = 'episode'
1573 self._doc_tree.SetFocus()
1574 self._rbtn_sort_by_episode.SetValue(True)
1575 #--------------------------------------------------------
1577 self._doc_tree.sort_mode = 'issue'
1578 self._doc_tree.SetFocus()
1579 self._rbtn_sort_by_issue.SetValue(True)
1580 #--------------------------------------------------------
1582 self._doc_tree.sort_mode = 'type'
1583 self._doc_tree.SetFocus()
1584 self._rbtn_sort_by_type.SetValue(True)
1585 #--------------------------------------------------------
1587 self._doc_tree.sort_mode = 'org'
1588 self._doc_tree.SetFocus()
1589 self._rbtn_sort_by_org.SetValue(True)
1590
1591 #--------------------------------------------------------
1593
1594 if (document is None) and (part is None):
1595 self._LCTRL_details.set_string_items([])
1596 return
1597
1598 if document is None:
1599 document = part.document
1600
1601 items = []
1602 if document is not None:
1603 items.append([_('Document'), '%s [#%s]' % (document['l10n_type'], document['pk_doc'])])
1604 items.append([_('Generated'), gmDateTime.pydt_strftime(document['clin_when'], '%Y %b %d')])
1605 items.append([_('Health issue'), gmTools.coalesce(document['health_issue'], '', '%%s [#%s]' % document['pk_health_issue'])])
1606 items.append([_('Episode'), '%s (%s) [#%s]' % (
1607 document['episode'],
1608 gmTools.bool2subst(document['episode_open'], _('open'), _('closed')),
1609 document['pk_episode']
1610 )])
1611 if document['pk_org_unit'] is not None:
1612 if document['unit_is_receiver']:
1613 header = _('Receiver')
1614 else:
1615 header = _('Sender')
1616 items.append([header, '%s @ %s' % (document['unit'], document['organization'])])
1617 if document['ext_ref'] is not None:
1618 items.append([_('Reference'), document['ext_ref']])
1619 if document['comment'] is not None:
1620 items.append([_('Comment'), ' / '.join(document['comment'].split('\n'))])
1621 for proc in document.procedures:
1622 items.append([_('Procedure'), proc.format (
1623 left_margin = 0,
1624 include_episode = False,
1625 include_codes = False,
1626 include_address = False,
1627 include_comm = False,
1628 include_doc = False
1629 )])
1630 stay = document.hospital_stay
1631 if stay is not None:
1632 items.append([_('Hospital stay'), stay.format(include_episode = False)])
1633 for bill in document.bills:
1634 items.append([_('Bill'), bill.format (
1635 include_receiver = False,
1636 include_doc = False
1637 )])
1638 items.append([_('Modified'), gmDateTime.pydt_strftime(document['modified_when'], '%Y %b %d')])
1639 items.append([_('... by'), document['modified_by']])
1640 items.append([_('# encounter'), document['pk_encounter']])
1641
1642 if part is not None:
1643 items.append(['', ''])
1644 if part['seq_idx'] is None:
1645 items.append([_('Part'), '#%s' % part['pk_obj']])
1646 else:
1647 items.append([_('Part'), '%s [#%s]' % (part['seq_idx'], part['pk_obj'])])
1648 if part['obj_comment'] is not None:
1649 items.append([_('Comment'), part['obj_comment']])
1650 if part['filename'] is not None:
1651 items.append([_('Filename'), part['filename']])
1652 items.append([_('Data size'), gmTools.size2str(part['size'])])
1653 review_parts = []
1654 if part['reviewed_by_you']:
1655 review_parts.append(_('by you'))
1656 if part['reviewed_by_intended_reviewer']:
1657 review_parts.append(_('by intended reviewer'))
1658 review = ', '.join(review_parts)
1659 if review == '':
1660 review = gmTools.u_diameter
1661 items.append([_('Reviewed'), review])
1662 #items.append([_(u'Reviewed'), gmTools.bool2subst(part['reviewed'], review, u'', u'?')])
1663
1664 self._LCTRL_details.set_string_items(items)
1665 self._LCTRL_details.set_column_widths()
1666 self._LCTRL_details.set_resize_column(1)
1667
1668 #============================================================
1670 """This wx.TreeCtrl derivative displays a tree view of stored medical documents.
1671
1672 It listens to document and patient changes and updates itself accordingly.
1673
1674 This acts on the current patient.
1675 """
1676 _sort_modes = ['age', 'review', 'episode', 'type', 'issue', 'org']
1677 _root_node_labels = None
1678
1679 #--------------------------------------------------------
1681 """Set up our specialised tree.
1682 """
1683 kwds['style'] = wx.TR_NO_BUTTONS | wx.NO_BORDER | wx.TR_SINGLE
1684 wx.TreeCtrl.__init__(self, parent, id, *args, **kwds)
1685
1686 gmRegetMixin.cRegetOnPaintMixin.__init__(self)
1687
1688 tmp = _('available documents (%s)')
1689 unsigned = _('unsigned (%s) on top') % '\u270D'
1690 cDocTree._root_node_labels = {
1691 'age': tmp % _('most recent on top'),
1692 'review': tmp % unsigned,
1693 'episode': tmp % _('sorted by episode'),
1694 'issue': tmp % _('sorted by health issue'),
1695 'type': tmp % _('sorted by type'),
1696 'org': tmp % _('sorted by organization')
1697 }
1698
1699 self.root = None
1700 self.__sort_mode = 'age'
1701
1702 self.__expanded_nodes = None
1703 self.__show_details_callback = None
1704
1705 self.__build_context_menus()
1706 self.__register_interests()
1707 self._schedule_data_reget()
1708
1709 #--------------------------------------------------------
1710 # external API
1711 #--------------------------------------------------------
1713
1714 node = self.GetSelection()
1715 node_data = self.GetItemData(node)
1716
1717 if not isinstance(node_data, gmDocuments.cDocumentPart):
1718 return True
1719
1720 self.__display_part(part = node_data)
1721 return True
1722
1723 #--------------------------------------------------------
1724 # properties
1725 #--------------------------------------------------------
1728
1730 if mode is None:
1731 mode = 'age'
1732
1733 if mode == self.__sort_mode:
1734 return
1735
1736 if mode not in cDocTree._sort_modes:
1737 raise ValueError('invalid document tree sort mode [%s], valid modes: %s' % (mode, cDocTree._sort_modes))
1738
1739 self.__sort_mode = mode
1740 self.__expanded_nodes = None
1741
1742 curr_pat = gmPerson.gmCurrentPatient()
1743 if not curr_pat.connected:
1744 return
1745
1746 self._schedule_data_reget()
1747
1748 sort_mode = property(_get_sort_mode, _set_sort_mode)
1749
1750 #--------------------------------------------------------
1752 if callback is not None:
1753 if not callable(callback):
1754 raise ValueError('<%s> is not callable')
1755 self.__show_details_callback = callback
1756
1757 show_details_callback = property(lambda x:x, _set_show_details_callback)
1758
1759 #--------------------------------------------------------
1760 # reget-on-paint API
1761 #--------------------------------------------------------
1763 curr_pat = gmPerson.gmCurrentPatient()
1764 if not curr_pat.connected:
1765 gmDispatcher.send(signal = 'statustext', msg = _('Cannot load documents. No active patient.'))
1766 return False
1767
1768 if not self.__populate_tree():
1769 return False
1770
1771 return True
1772
1773 #--------------------------------------------------------
1774 # internal helpers
1775 #--------------------------------------------------------
1777 # connect handlers
1778 self.Bind(wx.EVT_TREE_SEL_CHANGED, self._on_tree_item_selected)
1779 self.Bind(wx.EVT_TREE_ITEM_ACTIVATED, self._on_activate)
1780 self.Bind(wx.EVT_TREE_ITEM_RIGHT_CLICK, self.__on_right_click)
1781 self.Bind(wx.EVT_TREE_ITEM_GETTOOLTIP, self._on_tree_item_gettooltip)
1782 #wx.EVT_TREE_SEL_CHANGED (self, self.GetId(), self._on_tree_item_selected)
1783 #wx.EVT_TREE_ITEM_ACTIVATED (self, self.GetId(), self._on_activate)
1784 #wx.EVT_TREE_ITEM_RIGHT_CLICK (self, self.GetId(), self.__on_right_click)
1785 #wx.EVT_TREE_ITEM_GETTOOLTIP(self, -1, self._on_tree_item_gettooltip)
1786
1787 # wx.EVT_LEFT_DCLICK(self.tree, self.OnLeftDClick)
1788
1789 gmDispatcher.connect(signal = 'pre_patient_unselection', receiver = self._on_pre_patient_unselection)
1790 gmDispatcher.connect(signal = 'post_patient_selection', receiver = self._on_post_patient_selection)
1791 gmDispatcher.connect(signal = 'blobs.doc_med_mod_db', receiver = self._on_doc_mod_db)
1792 gmDispatcher.connect(signal = 'blobs.doc_obj_mod_db', receiver = self._on_doc_page_mod_db)
1793
1794 #--------------------------------------------------------
1857
1858 # document / description
1859 # self.__desc_menu = wx.Menu()
1860 # item = self.__doc_context_menu.Append(-1, _('Descriptions ...'), self.__desc_menu)
1861 # item = self.__desc_menu.Append(-1, _('Add new description'))
1862 # self.Bind(wx.EVT_MENU, self.__desc_menu, self.__add_doc_desc, item)
1863 # item = self.__desc_menu.Append(-1, _('Delete description'))
1864 # self.Bind(wx.EVT_MENU, self.__desc_menu, self.__del_doc_desc, item)
1865 # self.__desc_menu.AppendSeparator()
1866
1867 #--------------------------------------------------------
1869
1870 wx.BeginBusyCursor()
1871
1872 # clean old tree
1873 if self.root is not None:
1874 self.DeleteAllItems()
1875
1876 # init new tree
1877 self.root = self.AddRoot(cDocTree._root_node_labels[self.__sort_mode], -1, -1)
1878 self.SetItemData(self.root, None)
1879 self.SetItemHasChildren(self.root, False)
1880
1881 # read documents from database
1882 curr_pat = gmPerson.gmCurrentPatient()
1883 docs_folder = curr_pat.get_document_folder()
1884 docs = docs_folder.get_documents()
1885
1886 if docs is None:
1887 gmGuiHelpers.gm_show_error (
1888 aMessage = _('Error searching documents.'),
1889 aTitle = _('loading document list')
1890 )
1891 # avoid recursion of GUI updating
1892 wx.EndBusyCursor()
1893 return True
1894
1895 if len(docs) == 0:
1896 wx.EndBusyCursor()
1897 return True
1898
1899 # fill new tree from document list
1900 self.SetItemHasChildren(self.root, True)
1901
1902 # add our documents as first level nodes
1903 intermediate_nodes = {}
1904 for doc in docs:
1905
1906 parts = doc.parts
1907
1908 if len(parts) == 0:
1909 no_parts = _('no parts')
1910 elif len(parts) == 1:
1911 no_parts = _('1 part')
1912 else:
1913 no_parts = _('%s parts') % len(parts)
1914
1915 # need intermediate branch level ?
1916 if self.__sort_mode == 'episode':
1917 intermediate_label = '%s%s' % (doc['episode'], gmTools.coalesce(doc['health_issue'], '', ' (%s)'))
1918 doc_label = _('%s%7s %s:%s (%s)') % (
1919 gmTools.bool2subst(doc.has_unreviewed_parts, gmTools.u_writing_hand, '', '?'),
1920 doc['clin_when'].strftime('%m/%Y'),
1921 doc['l10n_type'][:26],
1922 gmTools.coalesce(initial = doc['comment'], instead = '', template_initial = ' %s'),
1923 no_parts
1924 )
1925 if intermediate_label not in intermediate_nodes:
1926 intermediate_nodes[intermediate_label] = self.AppendItem(parent = self.root, text = intermediate_label)
1927 self.SetItemBold(intermediate_nodes[intermediate_label], bold = True)
1928 self.SetItemData(intermediate_nodes[intermediate_label], None)
1929 self.SetItemHasChildren(intermediate_nodes[intermediate_label], True)
1930 parent = intermediate_nodes[intermediate_label]
1931
1932 elif self.__sort_mode == 'type':
1933 intermediate_label = doc['l10n_type']
1934 doc_label = _('%s%7s (%s):%s') % (
1935 gmTools.bool2subst(doc.has_unreviewed_parts, gmTools.u_writing_hand, '', '?'),
1936 doc['clin_when'].strftime('%m/%Y'),
1937 no_parts,
1938 gmTools.coalesce(initial = doc['comment'], instead = '', template_initial = ' %s')
1939 )
1940 if intermediate_label not in intermediate_nodes:
1941 intermediate_nodes[intermediate_label] = self.AppendItem(parent = self.root, text = intermediate_label)
1942 self.SetItemBold(intermediate_nodes[intermediate_label], bold = True)
1943 self.SetItemData(intermediate_nodes[intermediate_label], None)
1944 self.SetItemHasChildren(intermediate_nodes[intermediate_label], True)
1945 parent = intermediate_nodes[intermediate_label]
1946
1947 elif self.__sort_mode == 'issue':
1948 if doc['health_issue'] is None:
1949 intermediate_label = _('%s (unattributed episode)') % doc['episode']
1950 else:
1951 intermediate_label = doc['health_issue']
1952 doc_label = _('%s%7s %s:%s (%s)') % (
1953 gmTools.bool2subst(doc.has_unreviewed_parts, gmTools.u_writing_hand, '', '?'),
1954 doc['clin_when'].strftime('%m/%Y'),
1955 doc['l10n_type'][:26],
1956 gmTools.coalesce(initial = doc['comment'], instead = '', template_initial = ' %s'),
1957 no_parts
1958 )
1959 if intermediate_label not in intermediate_nodes:
1960 intermediate_nodes[intermediate_label] = self.AppendItem(parent = self.root, text = intermediate_label)
1961 self.SetItemBold(intermediate_nodes[intermediate_label], bold = True)
1962 self.SetItemData(intermediate_nodes[intermediate_label], None)
1963 self.SetItemHasChildren(intermediate_nodes[intermediate_label], True)
1964 parent = intermediate_nodes[intermediate_label]
1965
1966 elif self.__sort_mode == 'org':
1967 if doc['pk_org'] is None:
1968 intermediate_label = _('unknown organization')
1969 tt = ''
1970 else:
1971 if doc['unit_is_receiver']:
1972 direction = _('to: %s')
1973 else:
1974 direction = _('from: %s')
1975 # this praxis ?
1976 if doc['pk_org'] == gmPraxis.gmCurrentPraxisBranch()['pk_org']:
1977 org_str = _('this praxis')
1978 else:
1979 org_str = doc['organization']
1980 intermediate_label = direction % org_str
1981 # not quite right: always shows data of the _first_ document of _any_ org unit of this org
1982 tt = '\n'.join(doc.org_unit.format(with_address = True, with_org = True, with_comms = True))
1983 doc_label = _('%s%7s %s:%s (%s)') % (
1984 gmTools.bool2subst(doc.has_unreviewed_parts, gmTools.u_writing_hand, '', '?'),
1985 doc['clin_when'].strftime('%m/%Y'),
1986 doc['l10n_type'][:26],
1987 gmTools.coalesce(initial = doc['comment'], instead = '', template_initial = ' %s'),
1988 no_parts
1989 )
1990 if intermediate_label not in intermediate_nodes:
1991 intermediate_nodes[intermediate_label] = self.AppendItem(parent = self.root, text = intermediate_label)
1992 self.SetItemBold(intermediate_nodes[intermediate_label], bold = True)
1993 #self.SetItemData(intermediate_nodes[intermediate_label], None)
1994 self.SetItemData(intermediate_nodes[intermediate_label], tt)
1995 self.SetItemHasChildren(intermediate_nodes[intermediate_label], True)
1996 parent = intermediate_nodes[intermediate_label]
1997
1998 else:
1999 doc_label = _('%s%7s %s:%s (%s)') % (
2000 gmTools.bool2subst(doc.has_unreviewed_parts, gmTools.u_writing_hand, '', '?'),
2001 doc['clin_when'].strftime('%Y-%m'),
2002 doc['l10n_type'][:26],
2003 gmTools.coalesce(initial = doc['comment'], instead = '', template_initial = ' %s'),
2004 no_parts
2005 )
2006 parent = self.root
2007
2008 doc_node = self.AppendItem(parent = parent, text = doc_label)
2009 #self.SetItemBold(doc_node, bold = True)
2010 self.SetItemData(doc_node, doc)
2011 if len(parts) == 0:
2012 self.SetItemHasChildren(doc_node, False)
2013 else:
2014 self.SetItemHasChildren(doc_node, True)
2015
2016 # now add parts as child nodes
2017 for part in parts:
2018 f_ext = ''
2019 if part['filename'] is not None:
2020 f_ext = os.path.splitext(part['filename'])[1].strip('.').strip()
2021 if f_ext != '':
2022 f_ext = ' .' + f_ext.upper()
2023 label = '%s%s (%s%s)%s' % (
2024 gmTools.bool2str (
2025 boolean = part['reviewed'] or part['reviewed_by_you'] or part['reviewed_by_intended_reviewer'],
2026 true_str = '',
2027 false_str = gmTools.u_writing_hand
2028 ),
2029 _('part %2s') % part['seq_idx'],
2030 gmTools.size2str(part['size']),
2031 f_ext,
2032 gmTools.coalesce (
2033 part['obj_comment'],
2034 '',
2035 ': %s%%s%s' % (gmTools.u_left_double_angle_quote, gmTools.u_right_double_angle_quote)
2036 )
2037 )
2038
2039 part_node = self.AppendItem(parent = doc_node, text = label)
2040 self.SetItemData(part_node, part)
2041 self.SetItemHasChildren(part_node, False)
2042
2043 self.__sort_nodes()
2044 self.SelectItem(self.root)
2045
2046 # restore expansion state
2047 if self.__expanded_nodes is not None:
2048 self.ExpansionState = self.__expanded_nodes
2049 # but always expand root node
2050 self.Expand(self.root)
2051 # if no expansion state available then
2052 # expand intermediate nodes as well
2053 if self.__expanded_nodes is None:
2054 # but only if there are any
2055 if self.__sort_mode in ['episode', 'type', 'issue', 'org']:
2056 for key in intermediate_nodes.keys():
2057 self.Expand(intermediate_nodes[key])
2058
2059 wx.EndBusyCursor()
2060
2061 return True
2062
2063 #------------------------------------------------------------------------
2065 """Used in sorting items.
2066
2067 -1: 1 < 2
2068 0: 1 = 2
2069 1: 1 > 2
2070 """
2071 # Windows can send bogus events so ignore that
2072 if not node1:
2073 _log.debug('invalid node 1')
2074 return 0
2075 if not node2:
2076 _log.debug('invalid node 2')
2077 return 0
2078 if not node1.IsOk():
2079 _log.debug('no data on node 1')
2080 return 0
2081 if not node2.IsOk():
2082 _log.debug('no data on node 2')
2083 return 0
2084
2085 data1 = self.GetItemData(node1)
2086 data2 = self.GetItemData(node2)
2087
2088 # doc node
2089 if isinstance(data1, gmDocuments.cDocument):
2090
2091 date_field = 'clin_when'
2092 #date_field = 'modified_when'
2093
2094 if self.__sort_mode == 'age':
2095 # reverse sort by date
2096 if data1[date_field] > data2[date_field]:
2097 return -1
2098 if data1[date_field] == data2[date_field]:
2099 return 0
2100 return 1
2101
2102 elif self.__sort_mode == 'episode':
2103 if data1['episode'] < data2['episode']:
2104 return -1
2105 if data1['episode'] == data2['episode']:
2106 # inner sort: reverse by date
2107 if data1[date_field] > data2[date_field]:
2108 return -1
2109 if data1[date_field] == data2[date_field]:
2110 return 0
2111 return 1
2112 return 1
2113
2114 elif self.__sort_mode == 'issue':
2115 if data1['health_issue'] < data2['health_issue']:
2116 return -1
2117 if data1['health_issue'] == data2['health_issue']:
2118 # inner sort: reverse by date
2119 if data1[date_field] > data2[date_field]:
2120 return -1
2121 if data1[date_field] == data2[date_field]:
2122 return 0
2123 return 1
2124 return 1
2125
2126 elif self.__sort_mode == 'review':
2127 # equality
2128 if data1.has_unreviewed_parts == data2.has_unreviewed_parts:
2129 # inner sort: reverse by date
2130 if data1[date_field] > data2[date_field]:
2131 return -1
2132 if data1[date_field] == data2[date_field]:
2133 return 0
2134 return 1
2135 if data1.has_unreviewed_parts:
2136 return -1
2137 return 1
2138
2139 elif self.__sort_mode == 'type':
2140 if data1['l10n_type'] < data2['l10n_type']:
2141 return -1
2142 if data1['l10n_type'] == data2['l10n_type']:
2143 # inner sort: reverse by date
2144 if data1[date_field] > data2[date_field]:
2145 return -1
2146 if data1[date_field] == data2[date_field]:
2147 return 0
2148 return 1
2149 return 1
2150
2151 elif self.__sort_mode == 'org':
2152 if (data1['organization'] is None) and (data2['organization'] is None):
2153 return 0
2154 if (data1['organization'] is None) and (data2['organization'] is not None):
2155 return 1
2156 if (data1['organization'] is not None) and (data2['organization'] is None):
2157 return -1
2158 txt1 = '%s %s' % (data1['organization'], data1['unit'])
2159 txt2 = '%s %s' % (data2['organization'], data2['unit'])
2160 if txt1 < txt2:
2161 return -1
2162 if txt1 == txt2:
2163 # inner sort: reverse by date
2164 if data1[date_field] > data2[date_field]:
2165 return -1
2166 if data1[date_field] == data2[date_field]:
2167 return 0
2168 return 1
2169 return 1
2170
2171 else:
2172 _log.error('unknown document sort mode [%s], reverse-sorting by age', self.__sort_mode)
2173 # reverse sort by date
2174 if data1[date_field] > data2[date_field]:
2175 return -1
2176 if data1[date_field] == data2[date_field]:
2177 return 0
2178 return 1
2179
2180 # part node
2181 if isinstance(data1, gmDocuments.cDocumentPart):
2182 # compare sequence IDs (= "page" numbers)
2183 # FIXME: wrong order ?
2184 if data1['seq_idx'] < data2['seq_idx']:
2185 return -1
2186 if data1['seq_idx'] == data2['seq_idx']:
2187 return 0
2188 return 1
2189
2190 # else sort alphabetically
2191 if None in [data1, data2]:
2192 l1 = self.GetItemText(node1)
2193 l2 = self.GetItemText(node2)
2194 if l1 < l2:
2195 return -1
2196 if l1 == l2:
2197 return 0
2198 else:
2199 if data1 < data2:
2200 return -1
2201 if data1 == data2:
2202 return 0
2203 return 1
2204
2205 #------------------------------------------------------------------------
2206 # event handlers
2207 #------------------------------------------------------------------------
2211 #------------------------------------------------------------------------
2215 #------------------------------------------------------------------------
2217 # empty out tree
2218 if self.root is not None:
2219 self.DeleteAllItems()
2220 self.root = None
2221 #------------------------------------------------------------------------
2223 # FIXME: self.__load_expansion_history_from_db (but not apply it !)
2224 self.__expanded_nodes = None
2225 self._schedule_data_reget()
2226
2227 #--------------------------------------------------------
2229 node = event.GetItem()
2230 node_data = self.GetItemData(node)
2231
2232 # pseudo root node
2233 if node_data is None:
2234 self.__show_details_callback(document = None, part = None)
2235 return
2236
2237 # document node
2238 if isinstance(node_data, gmDocuments.cDocument):
2239 self.__show_details_callback(document = node_data, part = None)
2240 return
2241
2242 # string nodes are labels such as episodes which may or may not have children
2243 if isinstance(node_data, str):
2244 self.__show_details_callback(document = None, part = None)
2245 return
2246
2247 if isinstance(node_data, gmDocuments.cDocumentPart):
2248 doc = self.GetItemData(self.GetItemParent(node))
2249 self.__show_details_callback(document = doc, part = node_data)
2250 return
2251
2252 raise ValueError(_('invalid document tree node data type: %s') % type(node_data))
2253
2254 #------------------------------------------------------------------------
2256 node = event.GetItem()
2257 node_data = self.GetItemData(node)
2258
2259 # exclude pseudo root node
2260 if node_data is None:
2261 return None
2262
2263 # expand/collapse documents on activation
2264 if isinstance(node_data, gmDocuments.cDocument):
2265 self.Toggle(node)
2266 return True
2267
2268 # string nodes are labels such as episodes which may or may not have children
2269 if isinstance(node_data, str):
2270 self.Toggle(node)
2271 return True
2272
2273 if isinstance(node_data, gmDocuments.cDocumentPart):
2274 self.__display_part(part = node_data)
2275 return True
2276
2277 raise ValueError(_('invalid document tree node data type: %s') % type(node_data))
2278
2279 #--------------------------------------------------------
2281
2282 node = evt.GetItem()
2283 self.__curr_node_data = self.GetItemData(node)
2284
2285 # exclude pseudo root node
2286 if self.__curr_node_data is None:
2287 return None
2288
2289 # documents
2290 if isinstance(self.__curr_node_data, gmDocuments.cDocument):
2291 self.__handle_doc_context()
2292
2293 # parts
2294 if isinstance(self.__curr_node_data, gmDocuments.cDocumentPart):
2295 self.__handle_part_context()
2296
2297 del self.__curr_node_data
2298 evt.Skip()
2299
2300 #--------------------------------------------------------
2302 self.__curr_node_data.set_as_active_photograph()
2303 #--------------------------------------------------------
2306 #--------------------------------------------------------
2309 #--------------------------------------------------------
2312 #--------------------------------------------------------
2314
2315 item = event.GetItem()
2316
2317 if not item.IsOk():
2318 event.SetToolTip(' ')
2319 return
2320
2321 data = self.GetItemData(item)
2322
2323 # documents
2324 if isinstance(data, gmDocuments.cDocument):
2325 tt = data.format()
2326 # parts
2327 elif isinstance(data, gmDocuments.cDocumentPart):
2328 tt = data.format()
2329 # explicit tooltip strings
2330 elif isinstance(data, str):
2331 tt = data
2332 # other (root, intermediate nodes)
2333 else:
2334 tt = ''
2335
2336 event.SetToolTip(tt)
2337 #--------------------------------------------------------
2338 # internal API
2339 #--------------------------------------------------------
2341
2342 if start_node is None:
2343 start_node = self.GetRootItem()
2344
2345 # protect against empty tree where not even
2346 # a root node exists
2347 if not start_node.IsOk():
2348 return True
2349
2350 self.SortChildren(start_node)
2351
2352 child_node, cookie = self.GetFirstChild(start_node)
2353 while child_node.IsOk():
2354 self.__sort_nodes(start_node = child_node)
2355 child_node, cookie = self.GetNextChild(start_node, cookie)
2356
2357 return
2358 #--------------------------------------------------------
2361
2362 #--------------------------------------------------------
2364 ID = None
2365 # make active patient photograph
2366 if self.__curr_node_data['type'] == 'patient photograph':
2367 item = self.__part_context_menu.Append(-1, _('Activate as current photo'))
2368 self.Bind(wx.EVT_MENU, self.__activate_as_current_photo, item)
2369 ID = item.Id
2370
2371 self.PopupMenu(self.__part_context_menu, wx.DefaultPosition)
2372
2373 if ID is not None:
2374 self.__part_context_menu.Delete(ID)
2375
2376 #--------------------------------------------------------
2377 # part level context menu handlers
2378 #--------------------------------------------------------
2380 """Display document part."""
2381
2382 # sanity check
2383 if part['size'] == 0:
2384 _log.debug('cannot display part [%s] - 0 bytes', part['pk_obj'])
2385 gmGuiHelpers.gm_show_error (
2386 aMessage = _('Document part does not seem to exist in database !'),
2387 aTitle = _('showing document')
2388 )
2389 return None
2390
2391 wx.BeginBusyCursor()
2392
2393 cfg = gmCfg.cCfgSQL()
2394
2395 # determine database export chunk size
2396 chunksize = int(
2397 cfg.get2 (
2398 option = "horstspace.blob_export_chunk_size",
2399 workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace,
2400 bias = 'workplace',
2401 default = default_chunksize
2402 ))
2403
2404 # shall we force blocking during view ?
2405 block_during_view = bool( cfg.get2 (
2406 option = 'horstspace.document_viewer.block_during_view',
2407 workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace,
2408 bias = 'user',
2409 default = None
2410 ))
2411
2412 # display it
2413 successful, msg = part.display_via_mime (
2414 chunksize = chunksize,
2415 block = block_during_view
2416 )
2417
2418 wx.EndBusyCursor()
2419
2420 if not successful:
2421 gmGuiHelpers.gm_show_error (
2422 aMessage = _('Cannot display document part:\n%s') % msg,
2423 aTitle = _('showing document')
2424 )
2425 return None
2426
2427 # handle review after display
2428 # 0: never
2429 # 1: always
2430 # 2: if no review by myself exists yet
2431 # 3: if no review at all exists yet
2432 # 4: if no review by responsible reviewer
2433 review_after_display = int(cfg.get2 (
2434 option = 'horstspace.document_viewer.review_after_display',
2435 workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace,
2436 bias = 'user',
2437 default = 3
2438 ))
2439 if review_after_display == 1: # always review
2440 self.__review_part(part=part)
2441 elif review_after_display == 2: # review if no review by me exists
2442 review_by_me = [ rev for rev in part.get_reviews() if rev['is_your_review'] ]
2443 if len(review_by_me) == 0:
2444 self.__review_part(part = part)
2445 elif review_after_display == 3:
2446 if len(part.get_reviews()) == 0:
2447 self.__review_part(part = part)
2448 elif review_after_display == 4:
2449 reviewed_by_responsible = [ rev for rev in part.get_reviews() if rev['is_review_by_responsible_reviewer'] ]
2450 if len(reviewed_by_responsible) == 0:
2451 self.__review_part(part = part)
2452
2453 return True
2454 #--------------------------------------------------------
2456 dlg = cReviewDocPartDlg (
2457 parent = self,
2458 id = -1,
2459 part = part
2460 )
2461 dlg.ShowModal()
2462 dlg.Destroy()
2463 #--------------------------------------------------------
2465 target_doc = manage_documents (
2466 parent = self,
2467 msg = _('\nSelect the document into which to move the selected part !\n')
2468 )
2469 if target_doc is None:
2470 return
2471 if not self.__curr_node_data.reattach(pk_doc = target_doc['pk_doc']):
2472 gmGuiHelpers.gm_show_error (
2473 aMessage = _('Cannot move document part.'),
2474 aTitle = _('Moving document part')
2475 )
2476 #--------------------------------------------------------
2478 delete_it = gmGuiHelpers.gm_show_question (
2479 cancel_button = True,
2480 title = _('Deleting document part'),
2481 question = _(
2482 'Are you sure you want to delete the %s part #%s\n'
2483 '\n'
2484 '%s'
2485 'from the following document\n'
2486 '\n'
2487 ' %s (%s)\n'
2488 '%s'
2489 '\n'
2490 'Really delete ?\n'
2491 '\n'
2492 '(this action cannot be reversed)'
2493 ) % (
2494 gmTools.size2str(self.__curr_node_data['size']),
2495 self.__curr_node_data['seq_idx'],
2496 gmTools.coalesce(self.__curr_node_data['obj_comment'], '', ' "%s"\n\n'),
2497 self.__curr_node_data['l10n_type'],
2498 gmDateTime.pydt_strftime(self.__curr_node_data['date_generated'], format = '%Y-%m-%d', accuracy = gmDateTime.acc_days),
2499 gmTools.coalesce(self.__curr_node_data['doc_comment'], '', ' "%s"\n')
2500 )
2501 )
2502 if not delete_it:
2503 return
2504
2505 gmDocuments.delete_document_part (
2506 part_pk = self.__curr_node_data['pk_obj'],
2507 encounter_pk = gmPerson.gmCurrentPatient().emr.active_encounter['pk_encounter']
2508 )
2509 #--------------------------------------------------------
2511
2512 gmHooks.run_hook_script(hook = 'before_%s_doc_part' % action)
2513
2514 wx.BeginBusyCursor()
2515
2516 # detect wrapper
2517 found, external_cmd = gmShellAPI.detect_external_binary('gm-%s_doc' % action)
2518 if not found:
2519 found, external_cmd = gmShellAPI.detect_external_binary('gm-%s_doc.bat' % action)
2520 if not found:
2521 _log.error('neither of gm-%s_doc or gm-%s_doc.bat found', action, action)
2522 wx.EndBusyCursor()
2523 gmGuiHelpers.gm_show_error (
2524 _('Cannot %(l10n_action)s document part - %(l10n_action)s command not found.\n'
2525 '\n'
2526 'Either of gm-%(action)s_doc or gm-%(action)s_doc.bat\n'
2527 'must be in the execution path. The command will\n'
2528 'be passed the filename to %(l10n_action)s.'
2529 ) % {'action': action, 'l10n_action': l10n_action},
2530 _('Processing document part: %s') % l10n_action
2531 )
2532 return
2533
2534 cfg = gmCfg.cCfgSQL()
2535
2536 # determine database export chunk size
2537 chunksize = int(cfg.get2 (
2538 option = "horstspace.blob_export_chunk_size",
2539 workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace,
2540 bias = 'workplace',
2541 default = default_chunksize
2542 ))
2543
2544 part_file = self.__curr_node_data.save_to_file(aChunkSize = chunksize)
2545
2546 if action == 'print':
2547 cmd = '%s generic_document %s' % (external_cmd, part_file)
2548 else:
2549 cmd = '%s %s' % (external_cmd, part_file)
2550 if os.name == 'nt':
2551 blocking = True
2552 else:
2553 blocking = False
2554 success = gmShellAPI.run_command_in_shell (
2555 command = cmd,
2556 blocking = blocking
2557 )
2558
2559 wx.EndBusyCursor()
2560
2561 if not success:
2562 _log.error('%s command failed: [%s]', action, cmd)
2563 gmGuiHelpers.gm_show_error (
2564 _('Cannot %(l10n_action)s document part - %(l10n_action)s command failed.\n'
2565 '\n'
2566 'You may need to check and fix either of\n'
2567 ' gm-%(action)s_doc (Unix/Mac) or\n'
2568 ' gm-%(action)s_doc.bat (Windows)\n'
2569 '\n'
2570 'The command is passed the filename to %(l10n_action)s.'
2571 ) % {'action': action, 'l10n_action': l10n_action},
2572 _('Processing document part: %s') % l10n_action
2573 )
2574 else:
2575 if action == 'mail':
2576 curr_pat = gmPerson.gmCurrentPatient()
2577 emr = curr_pat.emr
2578 emr.add_clin_narrative (
2579 soap_cat = None,
2580 note = _('document part handed over to email program: %s') % self.__curr_node_data.format(single_line = True),
2581 episode = self.__curr_node_data['pk_episode']
2582 )
2583 #--------------------------------------------------------
2585 self.__process_part(action = 'print', l10n_action = _('print'))
2586 #--------------------------------------------------------
2588 self.__process_part(action = 'fax', l10n_action = _('fax'))
2589 #--------------------------------------------------------
2591 self.__process_part(action = 'mail', l10n_action = _('mail'))
2592 #--------------------------------------------------------
2594 """Save document part into directory."""
2595
2596 dlg = wx.DirDialog (
2597 parent = self,
2598 message = _('Save document part to directory ...'),
2599 defaultPath = os.path.expanduser(os.path.join('~', 'gnumed')),
2600 style = wx.DD_DEFAULT_STYLE
2601 )
2602 result = dlg.ShowModal()
2603 dirname = dlg.GetPath()
2604 dlg.Destroy()
2605
2606 if result != wx.ID_OK:
2607 return True
2608
2609 wx.BeginBusyCursor()
2610
2611 pat = gmPerson.gmCurrentPatient()
2612 fname = self.__curr_node_data.get_useful_filename (
2613 patient = pat,
2614 make_unique = True,
2615 directory = dirname
2616 )
2617
2618 cfg = gmCfg.cCfgSQL()
2619
2620 # determine database export chunk size
2621 chunksize = int(cfg.get2 (
2622 option = "horstspace.blob_export_chunk_size",
2623 workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace,
2624 bias = 'workplace',
2625 default = default_chunksize
2626 ))
2627
2628 fname = self.__curr_node_data.save_to_file (
2629 aChunkSize = chunksize,
2630 filename = fname,
2631 target_mime = None
2632 )
2633
2634 wx.EndBusyCursor()
2635
2636 gmDispatcher.send(signal = 'statustext', msg = _('Successfully saved document part as [%s].') % fname)
2637
2638 return True
2639
2640 #--------------------------------------------------------
2641 # document level context menu handlers
2642 #--------------------------------------------------------
2644 enc = gmEncounterWidgets.select_encounters (
2645 parent = self,
2646 patient = gmPerson.gmCurrentPatient()
2647 )
2648 if not enc:
2649 return
2650 self.__curr_node_data['pk_encounter'] = enc['pk_encounter']
2651 self.__curr_node_data.save()
2652 #--------------------------------------------------------
2654 enc = gmEMRStructItems.cEncounter(aPK_obj = self.__curr_node_data['pk_encounter'])
2655 gmEncounterWidgets.edit_encounter(parent = self, encounter = enc)
2656 #--------------------------------------------------------
2658
2659 gmHooks.run_hook_script(hook = 'before_%s_doc' % action)
2660
2661 wx.BeginBusyCursor()
2662
2663 # detect wrapper
2664 found, external_cmd = gmShellAPI.detect_external_binary('gm-%s_doc' % action)
2665 if not found:
2666 found, external_cmd = gmShellAPI.detect_external_binary('gm-%s_doc.bat' % action)
2667 if not found:
2668 _log.error('neither of gm-%s_doc or gm-%s_doc.bat found', action, action)
2669 wx.EndBusyCursor()
2670 gmGuiHelpers.gm_show_error (
2671 _('Cannot %(l10n_action)s document - %(l10n_action)s command not found.\n'
2672 '\n'
2673 'Either of gm-%(action)s_doc or gm-%(action)s_doc.bat\n'
2674 'must be in the execution path. The command will\n'
2675 'be passed a list of filenames to %(l10n_action)s.'
2676 ) % {'action': action, 'l10n_action': l10n_action},
2677 _('Processing document: %s') % l10n_action
2678 )
2679 return
2680
2681 cfg = gmCfg.cCfgSQL()
2682
2683 # determine database export chunk size
2684 chunksize = int(cfg.get2 (
2685 option = "horstspace.blob_export_chunk_size",
2686 workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace,
2687 bias = 'workplace',
2688 default = default_chunksize
2689 ))
2690
2691 part_files = self.__curr_node_data.save_parts_to_files(chunksize = chunksize)
2692
2693 if os.name == 'nt':
2694 blocking = True
2695 else:
2696 blocking = False
2697
2698 if action == 'print':
2699 cmd = '%s %s %s' % (
2700 external_cmd,
2701 'generic_document',
2702 ' '.join(part_files)
2703 )
2704 else:
2705 cmd = external_cmd + ' ' + ' '.join(part_files)
2706 success = gmShellAPI.run_command_in_shell (
2707 command = cmd,
2708 blocking = blocking
2709 )
2710
2711 wx.EndBusyCursor()
2712
2713 if not success:
2714 _log.error('%s command failed: [%s]', action, cmd)
2715 gmGuiHelpers.gm_show_error (
2716 _('Cannot %(l10n_action)s document - %(l10n_action)s command failed.\n'
2717 '\n'
2718 'You may need to check and fix either of\n'
2719 ' gm-%(action)s_doc (Unix/Mac) or\n'
2720 ' gm-%(action)s_doc.bat (Windows)\n'
2721 '\n'
2722 'The command is passed a list of filenames to %(l10n_action)s.'
2723 ) % {'action': action, 'l10n_action': l10n_action},
2724 _('Processing document: %s') % l10n_action
2725 )
2726
2727 #--------------------------------------------------------
2729 self.__process_doc(action = 'print', l10n_action = _('print'))
2730
2731 #--------------------------------------------------------
2733 self.__process_doc(action = 'fax', l10n_action = _('fax'))
2734
2735 #--------------------------------------------------------
2737 self.__process_doc(action = 'mail', l10n_action = _('mail'))
2738
2739 #--------------------------------------------------------
2741 dlg = wx.FileDialog (
2742 parent = self,
2743 message = _('Choose a file'),
2744 defaultDir = os.path.expanduser(os.path.join('~', 'gnumed')),
2745 defaultFile = '',
2746 wildcard = "%s (*)|*|PNGs (*.png)|*.png|PDFs (*.pdf)|*.pdf|TIFFs (*.tif)|*.tif|JPEGs (*.jpg)|*.jpg|%s (*.*)|*.*" % (_('all files'), _('all files (Win)')),
2747 style = wx.FD_OPEN | wx.FD_FILE_MUST_EXIST | wx.FD_MULTIPLE
2748 )
2749 result = dlg.ShowModal()
2750 if result != wx.ID_CANCEL:
2751 self.__curr_node_data.add_parts_from_files(files = dlg.GetPaths(), reviewer = gmStaff.gmCurrentProvider()['pk_staff'])
2752 dlg.Destroy()
2753
2754 #--------------------------------------------------------
2756 clip = gmGuiHelpers.clipboard2file()
2757 if clip is None:
2758 return
2759 if clip is False:
2760 return
2761 gmMimeLib.call_viewer_on_file(clip, block = False)
2762 really_add = gmGuiHelpers.gm_show_question (
2763 question = _('Really add the displayed clipboard item into the document ?'),
2764 title = _('Document part from clipboard')
2765 )
2766 if not really_add:
2767 return
2768 self.__curr_node_data.add_parts_from_files(files = [clip], reviewer = gmStaff.gmCurrentProvider()['pk_staff'])
2769 #--------------------------------------------------------
2771
2772 gmHooks.run_hook_script(hook = 'before_external_doc_access')
2773
2774 wx.BeginBusyCursor()
2775
2776 # detect wrapper
2777 found, external_cmd = gmShellAPI.detect_external_binary('gm_access_external_doc.sh')
2778 if not found:
2779 found, external_cmd = gmShellAPI.detect_external_binary('gm_access_external_doc.bat')
2780 if not found:
2781 _log.error('neither of gm_access_external_doc.sh or .bat found')
2782 wx.EndBusyCursor()
2783 gmGuiHelpers.gm_show_error (
2784 _('Cannot access external document - access command not found.\n'
2785 '\n'
2786 'Either of gm_access_external_doc.sh or *.bat must be\n'
2787 'in the execution path. The command will be passed the\n'
2788 'document type and the reference URL for processing.'
2789 ),
2790 _('Accessing external document')
2791 )
2792 return
2793
2794 cmd = '%s "%s" "%s"' % (external_cmd, self.__curr_node_data['type'], self.__curr_node_data['ext_ref'])
2795 if os.name == 'nt':
2796 blocking = True
2797 else:
2798 blocking = False
2799 success = gmShellAPI.run_command_in_shell (
2800 command = cmd,
2801 blocking = blocking
2802 )
2803
2804 wx.EndBusyCursor()
2805
2806 if not success:
2807 _log.error('External access command failed: [%s]', cmd)
2808 gmGuiHelpers.gm_show_error (
2809 _('Cannot access external document - access command failed.\n'
2810 '\n'
2811 'You may need to check and fix either of\n'
2812 ' gm_access_external_doc.sh (Unix/Mac) or\n'
2813 ' gm_access_external_doc.bat (Windows)\n'
2814 '\n'
2815 'The command is passed the document type and the\n'
2816 'external reference URL on the command line.'
2817 ),
2818 _('Accessing external document')
2819 )
2820 #--------------------------------------------------------
2822 """Save document into directory.
2823
2824 - one file per object
2825 - into subdirectory named after patient
2826 """
2827 pat = gmPerson.gmCurrentPatient()
2828 def_dir = os.path.expanduser(os.path.join('~', 'gnumed', pat.subdir_name))
2829 gmTools.mkdir(def_dir)
2830
2831 dlg = wx.DirDialog (
2832 parent = self,
2833 message = _('Save document into directory ...'),
2834 defaultPath = def_dir,
2835 style = wx.DD_DEFAULT_STYLE
2836 )
2837 result = dlg.ShowModal()
2838 dirname = dlg.GetPath()
2839 dlg.Destroy()
2840
2841 if result != wx.ID_OK:
2842 return True
2843
2844 wx.BeginBusyCursor()
2845
2846 cfg = gmCfg.cCfgSQL()
2847
2848 # determine database export chunk size
2849 chunksize = int(cfg.get2 (
2850 option = "horstspace.blob_export_chunk_size",
2851 workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace,
2852 bias = 'workplace',
2853 default = default_chunksize
2854 ))
2855
2856 fnames = self.__curr_node_data.save_parts_to_files(export_dir = dirname, chunksize = chunksize)
2857
2858 wx.EndBusyCursor()
2859
2860 gmDispatcher.send(signal='statustext', msg=_('Successfully saved %s parts into the directory [%s].') % (len(fnames), dirname))
2861
2862 return True
2863
2864 #--------------------------------------------------------
2867
2868 #--------------------------------------------------------
2870 delete_it = gmGuiHelpers.gm_show_question (
2871 aMessage = _('Are you sure you want to delete the document ?'),
2872 aTitle = _('Deleting document')
2873 )
2874 if delete_it is True:
2875 curr_pat = gmPerson.gmCurrentPatient()
2876 emr = curr_pat.emr
2877 enc = emr.active_encounter
2878 gmDocuments.delete_document(document_id = self.__curr_node_data['pk_doc'], encounter_id = enc['pk_encounter'])
2879
2880 #============================================================
2881 #============================================================
2882 # PACS
2883 #============================================================
2884 from Gnumed.wxGladeWidgets.wxgPACSPluginPnl import wxgPACSPluginPnl
2885
2887
2889 wxgPACSPluginPnl.__init__(self, *args, **kwargs)
2890 gmRegetMixin.cRegetOnPaintMixin.__init__(self)
2891 self.__pacs = None
2892 self.__patient = gmPerson.gmCurrentPatient()
2893 self.__orthanc_patient = None
2894 self.__image_data = None
2895
2896 self.__init_ui()
2897 self.__register_interests()
2898
2899 #--------------------------------------------------------
2900 # internal helpers
2901 #--------------------------------------------------------
2903 self._LCTRL_studies.set_columns(columns = [_('Date'), _('Description'), _('Organization'), _('Referrals')])
2904 self._LCTRL_studies.select_callback = self._on_studies_list_item_selected
2905 self._LCTRL_studies.deselect_callback = self._on_studies_list_item_deselected
2906
2907 self._LCTRL_series.set_columns(columns = [_('Time'), _('Method'), _('Body part'), _('Description')])
2908 self._LCTRL_series.select_callback = self._on_series_list_item_selected
2909 self._LCTRL_series.deselect_callback = self._on_series_list_item_deselected
2910
2911 self._LCTRL_details.set_columns(columns = [_('DICOM field'), _('Value')])
2912 self._LCTRL_details.set_column_widths()
2913
2914 self._BMP_preview.SetBitmap(wx.Bitmap.FromRGBA(50,50, red=0, green=0, blue=0, alpha = wx.ALPHA_TRANSPARENT))
2915
2916 #--------------------------------------------------------
2983
2984 #--------------------------------------------------------
2986 self._LBL_patient_identification.SetLabel('')
2987 self._LCTRL_studies.set_string_items(items = [])
2988 self._LCTRL_series.set_string_items(items = [])
2989 self.__refresh_image()
2990 self.__refresh_details()
2991
2992 #--------------------------------------------------------
2994 self._LBL_PACS_identification.SetLabel(_('<not connected>'))
2995
2996 #--------------------------------------------------------
2998 self.__reset_server_identification()
2999 self.__reset_patient_data()
3000 self.__set_button_states()
3001
3002 #-----------------------------------------------------
3004
3005 self.__pacs = None
3006 self.__orthanc_patient = None
3007 self.__set_button_states()
3008 self.__reset_server_identification()
3009
3010 host = self._TCTRL_host.Value.strip()
3011 port = self._TCTRL_port.Value.strip()[:6]
3012 if port == '':
3013 self._LBL_PACS_identification.SetLabel(_('Cannot connect without port (try 8042).'))
3014 return False
3015 if len(port) < 4:
3016 return False
3017 try:
3018 int(port)
3019 except ValueError:
3020 self._LBL_PACS_identification.SetLabel(_('Invalid port (try 8042).'))
3021 return False
3022
3023 user = self._TCTRL_user.Value
3024 if user == '':
3025 user = None
3026 self._LBL_PACS_identification.SetLabel(_('Connect to [%s] @ port %s as "%s".') % (host, port, user))
3027 password = self._TCTRL_password.Value
3028 if password == '':
3029 password = None
3030
3031 pacs = gmDICOM.cOrthancServer()
3032 if not pacs.connect(host = host, port = port, user = user, password = password): #, expected_aet = 'another AET'
3033 self._LBL_PACS_identification.SetLabel(_('Cannot connect to PACS.'))
3034 _log.error('error connecting to server: %s', pacs.connect_error)
3035 return False
3036
3037 #self._LBL_PACS_identification.SetLabel(_('PACS: Orthanc "%s" (AET "%s", Version %s, API v%s, DB v%s)') % (
3038 self._LBL_PACS_identification.SetLabel(_('PACS: Orthanc "%s" (AET "%s", Version %s, DB v%s)') % (
3039 pacs.server_identification['Name'],
3040 pacs.server_identification['DicomAet'],
3041 pacs.server_identification['Version'],
3042 #pacs.server_identification['ApiVersion'],
3043 pacs.server_identification['DatabaseVersion']
3044 ))
3045
3046 self.__pacs = pacs
3047 self.__set_button_states()
3048 return True
3049
3050 #--------------------------------------------------------
3052
3053 self.__orthanc_patient = None
3054
3055 if not self.__patient.connected:
3056 self.__reset_patient_data()
3057 self.__set_button_states()
3058 return True
3059
3060 if not self.__connect():
3061 return False
3062
3063 tt_lines = [_('Known PACS IDs:')]
3064 for pacs_id in self.__patient.suggest_external_ids(target = 'PACS'):
3065 tt_lines.append(' ' + _('generic: %s') % pacs_id)
3066 for pacs_id in self.__patient.get_external_ids(id_type = 'PACS', issuer = self.__pacs.as_external_id_issuer):
3067 tt_lines.append(' ' + _('stored: "%(value)s" @ [%(issuer)s]') % pacs_id)
3068 tt_lines.append('')
3069 tt_lines.append(_('Patients found in PACS:'))
3070
3071 info_lines = []
3072 # try to find patient
3073 matching_pats = self.__pacs.get_matching_patients(person = self.__patient)
3074 if len(matching_pats) == 0:
3075 info_lines.append(_('PACS: no patients with matching IDs found'))
3076 no_of_studies = 0
3077 for pat in matching_pats:
3078 info_lines.append('"%s" %s "%s (%s) %s"' % (
3079 pat['MainDicomTags']['PatientID'],
3080 gmTools.u_arrow2right,
3081 gmTools.coalesce(pat['MainDicomTags']['PatientName'], '?'),
3082 gmTools.coalesce(pat['MainDicomTags']['PatientSex'], '?'),
3083 gmTools.coalesce(pat['MainDicomTags']['PatientBirthDate'], '?')
3084 ))
3085 no_of_studies += len(pat['Studies'])
3086 tt_lines.append('%s [#%s]' % (
3087 gmTools.format_dict_like (
3088 pat['MainDicomTags'],
3089 relevant_keys = ['PatientName', 'PatientSex', 'PatientBirthDate', 'PatientID'],
3090 template = ' %(PatientID)s = %(PatientName)s (%(PatientSex)s) %(PatientBirthDate)s',
3091 missing_key_template = '?'
3092 ),
3093 pat['ID']
3094 ))
3095 if len(matching_pats) > 1:
3096 info_lines.append(_('PACS: more than one patient with matching IDs found, carefully check studies'))
3097 self._LBL_patient_identification.SetLabel('\n'.join(info_lines))
3098 tt_lines.append('')
3099 tt_lines.append(_('Studies found: %s') % no_of_studies)
3100 self._LBL_patient_identification.SetToolTip('\n'.join(tt_lines))
3101
3102 # get studies
3103 study_list_items = []
3104 study_list_data = []
3105 if len(matching_pats) > 0:
3106 # we don't at this point really expect more than one patient matching
3107 self.__orthanc_patient = matching_pats[0]
3108 for pat in self.__pacs.get_studies_list_by_orthanc_patient_list(orthanc_patients = matching_pats):
3109 for study in pat['studies']:
3110 docs = []
3111 if study['referring_doc'] is not None:
3112 docs.append(study['referring_doc'])
3113 if study['requesting_doc'] is not None:
3114 if study['requesting_doc'] not in docs:
3115 docs.append(study['requesting_doc'])
3116 if study['operator_name'] is not None:
3117 if study['operator_name'] not in docs:
3118 docs.append(study['operator_name'])
3119 study_list_items.append( [
3120 '%s-%s-%s' % (
3121 study['date'][:4],
3122 study['date'][4:6],
3123 study['date'][6:8]
3124 ),
3125 _('%s series%s') % (
3126 len(study['series']),
3127 gmTools.coalesce(study['description'], '', ': %s')
3128 ),
3129 gmTools.coalesce(study['radiology_org'], ''),
3130 gmTools.u_arrow2right.join(docs)
3131 ] )
3132 study_list_data.append(study)
3133
3134 self._LCTRL_studies.set_string_items(items = study_list_items)
3135 self._LCTRL_studies.set_data(data = study_list_data)
3136 self._LCTRL_studies.SortListItems(0, 0)
3137 self._LCTRL_studies.set_column_widths()
3138
3139 self.__refresh_image()
3140 self.__refresh_details()
3141 self.__set_button_states()
3142
3143 self.Layout()
3144 return True
3145
3146 #--------------------------------------------------------
3148
3149 self._LCTRL_details.remove_items_safely()
3150 if self.__pacs is None:
3151 return
3152
3153 # study available ?
3154 study_data = self._LCTRL_studies.get_selected_item_data(only_one = True)
3155 if study_data is None:
3156 return
3157 items = []
3158 items = [ [key, study_data['all_tags'][key]] for key in study_data['all_tags'] if ('%s' % study_data['all_tags'][key]).strip() != '' ]
3159
3160 # series available ?
3161 series = self._LCTRL_series.get_selected_item_data(only_one = True)
3162 if series is None:
3163 self._LCTRL_details.set_string_items(items = items)
3164 self._LCTRL_details.set_column_widths()
3165 return
3166 items.append([' ----- ', '--- %s ----------' % _('Series')])
3167 items.extend([ [key, series['all_tags'][key]] for key in series['all_tags'] if ('%s' % series['all_tags'][key]).strip() != '' ])
3168
3169 # image available ?
3170 if self.__image_data is None:
3171 self._LCTRL_details.set_string_items(items = items)
3172 self._LCTRL_details.set_column_widths()
3173 return
3174 tags = self.__pacs.get_instance_dicom_tags(instance_id = self.__image_data['uuid'])
3175 items.append([' ----- ', '--- %s ----------' % _('Image')])
3176 items.extend([ [key, tags[key]] for key in tags if ('%s' % tags[key]).strip() != '' ])
3177
3178 self._LCTRL_details.set_string_items(items = items)
3179 self._LCTRL_details.set_column_widths()
3180
3181 #--------------------------------------------------------
3183
3184 self.__image_data = None
3185 self._LBL_image.Label = _('Image')
3186 self._BMP_preview.SetBitmap(wx.Bitmap.FromRGBA(50,50, red=0, green=0, blue=0, alpha = wx.ALPHA_TRANSPARENT))
3187
3188 if idx is None:
3189 return
3190 if self.__pacs is None:
3191 return
3192 series = self._LCTRL_series.get_selected_item_data(only_one = True)
3193 if series is None:
3194 return
3195 if idx > len(series['instances']) - 1:
3196 raise ValueError('trying to go beyond instances in series: %s of %s', idx, len(series['instances']))
3197
3198 # get image
3199 uuid = series['instances'][idx]
3200 img_file = self.__pacs.get_instance_preview(instance_id = uuid)
3201 # scale
3202 wx_bmp = gmGuiHelpers.file2scaled_image(filename = img_file, height = 100)
3203 # show
3204 if wx_bmp is None:
3205 _log.error('cannot load DICOM instance from PACS: %s', uuid)
3206 else:
3207 self.__image_data = {'idx': idx, 'uuid': uuid}
3208 self._BMP_preview.SetBitmap(wx_bmp)
3209 self._LBL_image.Label = _('Image %s/%s') % (idx+1, len(series['instances']))
3210
3211 if idx == 0:
3212 self._BTN_previous_image.Disable()
3213 else:
3214 self._BTN_previous_image.Enable()
3215 if idx == len(series['instances']) - 1:
3216 self._BTN_next_image.Disable()
3217 else:
3218 self._BTN_next_image.Enable()
3219
3220 #--------------------------------------------------------
3221 # reget-on-paint mixin API
3222 #--------------------------------------------------------
3224 if not self.__patient.connected:
3225 self.__reset_ui_content()
3226 return True
3227
3228 if not self.__refresh_patient_data():
3229 return False
3230
3231 return True
3232
3233 #--------------------------------------------------------
3234 # event handling
3235 #--------------------------------------------------------
3237 # client internal signals
3238 gmDispatcher.connect(signal = 'pre_patient_unselection', receiver = self._on_pre_patient_unselection)
3239 gmDispatcher.connect(signal = 'post_patient_selection', receiver = self._on_post_patient_selection)
3240
3241 # generic database change signal
3242 gmDispatcher.connect(signal = 'gm_table_mod', receiver = self._on_database_signal)
3243
3244 #--------------------------------------------------------
3246 # only empty out here, do NOT access the patient
3247 # or else we will access the old patient while it
3248 # may not be valid anymore ...
3249 self.__reset_patient_data()
3250
3251 #--------------------------------------------------------
3254
3255 #--------------------------------------------------------
3257
3258 if not self.__patient.connected:
3259 # probably not needed:
3260 #self._schedule_data_reget()
3261 return True
3262
3263 if kwds['pk_identity'] != self.__patient.ID:
3264 return True
3265
3266 if kwds['table'] == 'dem.lnk_identity2ext_id':
3267 self._schedule_data_reget()
3268 return True
3269
3270 return True
3271
3272 #--------------------------------------------------------
3273 # events: lists
3274 #--------------------------------------------------------
3276
3277 event.Skip()
3278 if self.__pacs is None:
3279 return
3280
3281 study_data = self._LCTRL_studies.get_selected_item_data(only_one = True)
3282 if study_data is None:
3283 return
3284
3285 series = self._LCTRL_series.get_selected_item_data(only_one = True)
3286 if series is None:
3287 self.__set_button_states()
3288 return
3289
3290 if len(series['instances']) == 0:
3291 self.__refresh_image()
3292 self.__refresh_details()
3293 self.__set_button_states()
3294 return
3295
3296 # set first image
3297 self.__refresh_image(0)
3298 self.__refresh_details()
3299 self.__set_button_states()
3300 self._BTN_previous_image.Disable()
3301
3302 self.Layout()
3303
3304 #--------------------------------------------------------
3306 event.Skip()
3307
3308 self.__refresh_image()
3309 self.__refresh_details()
3310 self.__set_button_states()
3311
3312 #--------------------------------------------------------
3314 event.Skip()
3315 if self.__pacs is None:
3316 return
3317
3318 study_data = self._LCTRL_studies.get_selected_item_data(only_one = True)
3319 if study_data is None:
3320 self.__set_button_states()
3321 return
3322
3323 series_list_items = []
3324 series_list_data = []
3325 for series in study_data['series']:
3326
3327 series_time = ''
3328 if series['time'] is None:
3329 series['time'] = study_data['time']
3330 series_time = '%s:%s:%s' % (
3331 series['time'][:2],
3332 series['time'][2:4],
3333 series['time'][4:6]
3334 )
3335
3336 series_desc_parts = []
3337 if series['description'] is not None:
3338 if series['protocol'] is None:
3339 series_desc_parts.append(series['description'].strip())
3340 else:
3341 if series['description'].strip() not in series['protocol'].strip():
3342 series_desc_parts.append(series['description'].strip())
3343 if series['protocol'] is not None:
3344 series_desc_parts.append('[%s]' % series['protocol'].strip())
3345 if series['performed_procedure_step_description'] is not None:
3346 series_desc_parts.append(series['performed_procedure_step_description'].strip())
3347 if series['acquisition_device_processing_description'] is not None:
3348 series_desc_parts.append(series['acquisition_device_processing_description'].strip())
3349 series_desc = ' / '.join(series_desc_parts)
3350 if len(series_desc) > 0:
3351 series_desc = ': ' + series_desc
3352 series_desc = _('%s image(s)%s') % (len(series['instances']), series_desc)
3353
3354 series_list_items.append ([
3355 series_time,
3356 gmTools.coalesce(series['modality'], ''),
3357 gmTools.coalesce(series['body_part'], ''),
3358 series_desc
3359 ])
3360 series_list_data.append(series)
3361
3362 self._LCTRL_series.set_string_items(items = series_list_items)
3363 self._LCTRL_series.set_data(data = series_list_data)
3364 self._LCTRL_series.SortListItems(0)
3365
3366 self.__refresh_image()
3367 self.__refresh_details()
3368 self.__set_button_states()
3369
3370 #--------------------------------------------------------
3372 event.Skip()
3373
3374 self._LCTRL_series.remove_items_safely()
3375 self.__refresh_image()
3376 self.__refresh_details()
3377 self.__set_button_states()
3378
3379 #--------------------------------------------------------
3380 # events: buttons
3381 #--------------------------------------------------------
3396
3397 #--------------------------------------------------------
3403
3404 #--------------------------------------------------------
3410
3411 #--------------------------------------------------------
3420
3421 #--------------------------------------------------------
3466
3467 #--------------------------------------------------------
3484
3485 #--------------------------------------------------------
3486 # - image buttons
3487 #--------------------------------------------------------
3493
3494 #--------------------------------------------------------
3500
3501 #--------------------------------------------------------
3515
3516 #--------------------------------------------------------
3529
3530 #--------------------------------------------------------
3543
3544 #--------------------------------------------------------
3557
3558 #--------------------------------------------------------
3580
3581 #--------------------------------------------------------
3582 # - study buttons
3583 #--------------------------------------------------------
3617
3618 #--------------------------------------------------------
3644
3645 #--------------------------------------------------------
3646 # - patient buttons (= all studies)
3647 #--------------------------------------------------------
3690
3691 #--------------------------------------------------------
3723
3724 #--------------------------------------------------------
3749
3750 # #--------------------------------------------------------
3751 # # check size and confirm if huge
3752 # zip_size = os.path.getsize(filename)
3753 # if zip_size > (300 * gmTools._MB): # ~ 1/2 CD-ROM
3754 # really_export = gmGuiHelpers.gm_show_question (
3755 # title = _('Exporting DICOM studies'),
3756 # question = _('The DICOM studies are %s in compressed size.\n\nReally copy to export area ?') % gmTools.size2str(zip_size),
3757 # cancel_button = False
3758 # )
3759 # if not really_export:
3760 # wx.BeginBusyCursor()
3761 # gmTools.remove_file(filename)
3762 # wx.EndBusyCursor()
3763 # return
3764 #
3765 # # import into export area
3766 # wx.BeginBusyCursor()
3767 # self.__patient.export_area.add_file (
3768 # filename = filename,
3769 # hint = _('All DICOM studies of [%s] from Orthanc PACS "%s" (AET "%s")') % (
3770 # self.__orthanc_patient['MainDicomTags']['PatientID'],
3771 # self.__pacs.server_identification['Name'],
3772 # self.__pacs.server_identification['DicomAet']
3773 # )
3774 # )
3775 # gmTools.remove_file(filename)
3776 # wx.EndBusyCursor()
3777 #
3778 # def _on_export_study_button_pressed(self, event):
3779 # event.Skip()
3780 # if self.__pacs is None:
3781 # return
3782 #
3783 # study_data = self._LCTRL_studies.get_selected_item_data(only_one = False)
3784 # if len(study_data) == 0:
3785 # return
3786 #
3787 # wx.BeginBusyCursor()
3788 # filename = self.__pacs.get_studies_with_dicomdir(study_ids = [ s['orthanc_id'] for s in study_data ], create_zip = True)
3789 # wx.EndBusyCursor()
3790 #
3791 # if filename is False:
3792 # gmGuiHelpers.gm_show_error (
3793 # title = _('Exporting DICOM studies'),
3794 # error = _('Unable to export selected studies.')
3795 # )
3796 # return
3797 #
3798 # # check size and confirm if huge
3799 # zip_size = os.path.getsize(filename)
3800 # if zip_size > (300 * gmTools._MB): # ~ 1/2 CD-ROM
3801 # really_export = gmGuiHelpers.gm_show_question (
3802 # title = _('Exporting DICOM studies'),
3803 # question = _('The DICOM studies are %s in compressed size.\n\nReally copy to export area ?') % gmTools.size2str(zip_size),
3804 # cancel_button = False
3805 # )
3806 # if not really_export:
3807 # wx.BeginBusyCursor()
3808 # gmTools.remove_file(filename)
3809 # wx.EndBusyCursor()
3810 # return
3811 #
3812 # # import into export area
3813 # wx.BeginBusyCursor()
3814 # self.__patient.export_area.add_file (
3815 # filename = filename,
3816 # hint = _('DICOM studies of [%s] from Orthanc PACS "%s" (AET "%s")') % (
3817 # self.__orthanc_patient['MainDicomTags']['PatientID'],
3818 # self.__pacs.server_identification['Name'],
3819 # self.__pacs.server_identification['DicomAet']
3820 # )
3821 # )
3822 # gmTools.remove_file(filename)
3823 # wx.EndBusyCursor()
3824
3825 #------------------------------------------------------------
3826 from Gnumed.wxGladeWidgets.wxgModifyOrthancContentDlg import wxgModifyOrthancContentDlg
3827
3830 self.__srv = kwds['server']
3831 del kwds['server']
3832 title = kwds['title']
3833 del kwds['title']
3834 wxgModifyOrthancContentDlg.__init__(self, *args, **kwds)
3835 self.SetTitle(title)
3836 self._LCTRL_patients.set_columns( [_('Patient ID'), _('Name'), _('Birth date'), _('Gender'), _('Orthanc')] )
3837
3838 #--------------------------------------------------------
3840 self._LCTRL_patients.set_string_items()
3841 search_term = self._TCTRL_search_term.Value.strip()
3842 if search_term == '':
3843 return
3844 pats = self.__srv.get_patients_by_name(name_parts = search_term.split(), fuzzy = True)
3845 if len(pats) == 0:
3846 return
3847 list_items = []
3848 list_data = []
3849 for pat in pats:
3850 mt = pat['MainDicomTags']
3851 try:
3852 gender = mt['PatientSex']
3853 except KeyError:
3854 gender = ''
3855 try:
3856 dob = mt['PatientBirthDate']
3857 except KeyError:
3858 dob = ''
3859 list_items.append([mt['PatientID'], mt['PatientName'], dob, gender, pat['ID']])
3860 list_data.append(mt['PatientID'])
3861 self._LCTRL_patients.set_string_items(list_items)
3862 self._LCTRL_patients.set_column_widths()
3863 self._LCTRL_patients.set_data(list_data)
3864
3865 #--------------------------------------------------------
3869
3870 #--------------------------------------------------------
3877
3878 #--------------------------------------------------------
3918
3919 #------------------------------------------------------------
3920 # outdated:
3922 event.Skip()
3923 dlg = wx.DirDialog (
3924 self,
3925 message = _('Select the directory from which to recursively upload DICOM files.'),
3926 defaultPath = os.path.join(gmTools.gmPaths().home_dir, 'gnumed')
3927 )
3928 choice = dlg.ShowModal()
3929 dicom_dir = dlg.GetPath()
3930 dlg.Destroy()
3931 if choice != wx.ID_OK:
3932 return True
3933 wx.BeginBusyCursor()
3934 try:
3935 uploaded, not_uploaded = self.__pacs.upload_from_directory (
3936 directory = dicom_dir,
3937 recursive = True,
3938 check_mime_type = False,
3939 ignore_other_files = True
3940 )
3941 finally:
3942 wx.EndBusyCursor()
3943 if len(not_uploaded) == 0:
3944 q = _('Delete the uploaded DICOM files now ?')
3945 else:
3946 q = _('Some files have not been uploaded.\n\nDo you want to delete those DICOM files which have been sent to the PACS successfully ?')
3947 _log.error('not uploaded:')
3948 for f in not_uploaded:
3949 _log.error(f)
3950 delete_uploaded = gmGuiHelpers.gm_show_question (
3951 title = _('Uploading DICOM files'),
3952 question = q,
3953 cancel_button = False
3954 )
3955 if not delete_uploaded:
3956 return
3957 wx.BeginBusyCursor()
3958 for f in uploaded:
3959 gmTools.remove_file(f)
3960 wx.EndBusyCursor()
3961
3962 #============================================================
3963 # main
3964 #------------------------------------------------------------
3965 if __name__ == '__main__':
3966
3967 if len(sys.argv) < 2:
3968 sys.exit()
3969
3970 if sys.argv[1] != 'test':
3971 sys.exit()
3972
3973 from Gnumed.business import gmPersonSearch
3974 from Gnumed.wxpython import gmPatSearchWidgets
3975
3976 #----------------------------------------------------------------
3978 app = wx.PyWidgetTester(size = (180, 20))
3979 #pnl = cEncounterEditAreaPnl(app.frame, -1, encounter=enc)
3980 prw = cDocumentPhraseWheel(app.frame, -1)
3981 prw.set_context('pat', 12)
3982 app.frame.Show(True)
3983 app.MainLoop()
3984
3985 #----------------------------------------------------------------
3986 test_document_prw()
3987
| Home | Trees | Indices | Help |
|
|---|
| Generated by Epydoc 3.0.1 on Thu May 10 01:55:20 2018 | http://epydoc.sourceforge.net |