1
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 import datetime as pydt
17
18
19 import wx
20 import wx.lib.mixins.treemixin as treemixin
21
22
23 if __name__ == '__main__':
24 sys.path.insert(0, '../../')
25 from Gnumed.pycommon import gmI18N
26 if __name__ == '__main__':
27 gmI18N.activate_locale()
28 gmI18N.install_domain(domain = 'gnumed')
29 from Gnumed.pycommon import gmCfg
30 from Gnumed.pycommon import gmCfg2
31 from Gnumed.pycommon import gmPG2
32 from Gnumed.pycommon import gmMimeLib
33 from Gnumed.pycommon import gmMatchProvider
34 from Gnumed.pycommon import gmDispatcher
35 from Gnumed.pycommon import gmDateTime
36 from Gnumed.pycommon import gmTools
37 from Gnumed.pycommon import gmShellAPI
38 from Gnumed.pycommon import gmHooks
39 from Gnumed.pycommon import gmNetworkTools
40 from Gnumed.pycommon import gmMimeLib
41 from Gnumed.pycommon import gmConnectionPool
42
43 from Gnumed.business import gmPerson
44 from Gnumed.business import gmStaff
45 from Gnumed.business import gmDocuments
46 from Gnumed.business import gmEMRStructItems
47 from Gnumed.business import gmPraxis
48 from Gnumed.business import gmDICOM
49 from Gnumed.business import gmProviderInbox
50 from Gnumed.business import gmOrganization
51
52 from Gnumed.wxpython import gmGuiHelpers
53 from Gnumed.wxpython import gmRegetMixin
54 from Gnumed.wxpython import gmPhraseWheel
55 from Gnumed.wxpython import gmPlugin
56 from Gnumed.wxpython import gmEncounterWidgets
57 from Gnumed.wxpython import gmListWidgets
58 from Gnumed.wxpython import gmRegetMixin
59
60
61 _log = logging.getLogger('gm.ui')
62
63
64 default_chunksize = 1 * 1024 * 1024
65
66
68
69
70 def delete_item(item):
71 doit = gmGuiHelpers.gm_show_question (
72 _( 'Are you sure you want to delete this\n'
73 'description from the document ?\n'
74 ),
75 _('Deleting document description')
76 )
77 if not doit:
78 return True
79
80 document.delete_description(pk = item[0])
81 return True
82
83 def add_item():
84 dlg = gmGuiHelpers.cMultilineTextEntryDlg (
85 parent,
86 -1,
87 title = _('Adding document description'),
88 msg = _('Below you can add a document description.\n')
89 )
90 result = dlg.ShowModal()
91 if result == wx.ID_SAVE:
92 document.add_description(dlg.value)
93
94 dlg.DestroyLater()
95 return True
96
97 def edit_item(item):
98 dlg = gmGuiHelpers.cMultilineTextEntryDlg (
99 parent,
100 -1,
101 title = _('Editing document description'),
102 msg = _('Below you can edit the document description.\n'),
103 text = item[1]
104 )
105 result = dlg.ShowModal()
106 if result == wx.ID_SAVE:
107 document.update_description(pk = item[0], description = dlg.value)
108
109 dlg.DestroyLater()
110 return True
111
112 def refresh_list(lctrl):
113 descriptions = document.get_descriptions()
114
115 lctrl.set_string_items(items = [
116 '%s%s' % ( (' '.join(regex.split('\r\n+|\r+|\n+|\t+', desc[1])))[:30], gmTools.u_ellipsis )
117 for desc in descriptions
118 ])
119 lctrl.set_data(data = descriptions)
120
121
122 gmListWidgets.get_choices_from_list (
123 parent = parent,
124 msg = _('Select the description you are interested in.\n'),
125 caption = _('Managing document descriptions'),
126 columns = [_('Description')],
127 edit_callback = edit_item,
128 new_callback = add_item,
129 delete_callback = delete_item,
130 refresh_callback = refresh_list,
131 single_selection = True,
132 can_return_empty = True
133 )
134
135 return True
136
137
139 try:
140 del kwargs['signal']
141 del kwargs['sender']
142 except KeyError:
143 pass
144 wx.CallAfter(save_file_as_new_document, **kwargs)
145
147 try:
148 del kwargs['signal']
149 del kwargs['sender']
150 except KeyError:
151 pass
152 wx.CallAfter(save_files_as_new_document, **kwargs)
153
154
155 -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):
165
166
167 -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):
168
169 pat = gmPerson.gmCurrentPatient()
170 if not pat.connected:
171 return None
172
173 emr = pat.emr
174 if parent is None:
175 parent = wx.GetApp().GetTopWindow()
176
177 if episode is None:
178 all_epis = emr.get_episodes()
179 if len(all_epis) == 0:
180 episode = emr.add_episode(episode_name = _('Documents'), is_open = False)
181 else:
182 from Gnumed.wxpython.gmEMRStructWidgets import cEpisodeListSelectorDlg
183 dlg = cEpisodeListSelectorDlg(parent, -1, episodes = all_epis)
184 dlg.SetTitle(_('Select the episode under which to file the document ...'))
185 btn_pressed = dlg.ShowModal()
186 episode = dlg.get_selected_item_data(only_one = True)
187 dlg.DestroyLater()
188 if (btn_pressed == wx.ID_CANCEL) or (episode is None):
189 if unlock_patient:
190 pat.locked = False
191 return None
192
193 wx.BeginBusyCursor()
194 if pk_document_type is None:
195 pk_document_type = gmDocuments.create_document_type(document_type = document_type)['pk_doc_type']
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 None
209
210 doc['ext_ref'] = reference
211 doc['pk_org_unit'] = pk_org_unit
212 doc['clin_when'] = date_generated
213 doc['comment'] = gmTools.none_if(value = comment, none_equivalent = '', strip_string = True)
214 doc.save()
215 success, msg, filename = doc.add_parts_from_files(files = filenames, reviewer = reviewer)
216 if not success:
217 wx.EndBusyCursor()
218 gmGuiHelpers.gm_show_error (
219 aMessage = msg,
220 aTitle = _('saving document')
221 )
222 return None
223
224 if review_as_normal:
225 doc.set_reviewed(technically_abnormal = False, clinically_relevant = False)
226 if unlock_patient:
227 pat.locked = False
228
229 gmDispatcher.send(signal = 'statustext', msg = _('Imported new document from %s.') % filenames, beep = True)
230 cfg = gmCfg.cCfgSQL()
231 show_id = bool (
232 cfg.get2 (
233 option = 'horstspace.scan_index.show_doc_id',
234 workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace,
235 bias = 'user'
236 )
237 )
238 wx.EndBusyCursor()
239 if not show_id:
240 gmDispatcher.send(signal = 'statustext', msg = _('Successfully saved new document.'))
241 else:
242 if reference is None:
243 msg = _('Successfully saved the new document.')
244 else:
245 msg = _('The reference ID for the new document is:\n'
246 '\n'
247 ' <%s>\n'
248 '\n'
249 'You probably want to write it down on the\n'
250 'original documents.\n'
251 '\n'
252 "If you don't care about the ID you can switch\n"
253 'off this message in the GNUmed configuration.\n'
254 ) % reference
255 gmGuiHelpers.gm_show_info (
256 aMessage = msg,
257 aTitle = _('Saving document')
258 )
259
260 tmp_dir = gmTools.gmPaths().tmp_dir
261 files2remove = [ f for f in filenames if not f.startswith(tmp_dir) ]
262 if len(files2remove) > 0:
263 do_delete = gmGuiHelpers.gm_show_question (
264 _( 'Successfully imported files as document.\n'
265 '\n'
266 'Do you want to delete imported files from the filesystem ?\n'
267 '\n'
268 ' %s'
269 ) % '\n '.join(files2remove),
270 _('Removing files')
271 )
272 if do_delete:
273 for fname in files2remove:
274 gmTools.remove_file(fname)
275
276 return doc
277
278
279 gmDispatcher.connect(signal = 'import_document_from_file', receiver = _save_file_as_new_document)
280 gmDispatcher.connect(signal = 'import_document_from_files', receiver = _save_files_as_new_document)
281
282
284
286
287 gmPhraseWheel.cPhraseWheel.__init__(self, *args, **kwargs)
288
289 ctxt = {'ctxt_pat': {
290 'where_part': '(pk_patient = %(pat)s) AND',
291 'placeholder': 'pat'
292 }}
293
294 mp = gmMatchProvider.cMatchProvider_SQL2 (
295 queries = ["""
296 SELECT DISTINCT ON (list_label)
297 pk_doc AS data,
298 l10n_type || ' (' || to_char(clin_when, 'YYYY Mon DD') || ')' || coalesce(': ' || unit || '@' || organization, '') || ' - ' || episode || coalesce(' (' || health_issue || ')', '') AS list_label,
299 l10n_type || ' (' || to_char(clin_when, 'YYYY Mon DD') || ')' || coalesce(': ' || organization, '') || ' - ' || coalesce(' (' || health_issue || ')', episode) AS field_label
300 FROM blobs.v_doc_med
301 WHERE
302 %(ctxt_pat)s
303 (
304 l10n_type %(fragment_condition)s
305 OR
306 unit %(fragment_condition)s
307 OR
308 organization %(fragment_condition)s
309 OR
310 episode %(fragment_condition)s
311 OR
312 health_issue %(fragment_condition)s
313 )
314 ORDER BY list_label
315 LIMIT 25"""],
316 context = ctxt
317 )
318 mp.setThresholds(1, 3, 5)
319
320 pat = gmPerson.gmCurrentPatient()
321 mp.set_context('pat', pat.ID)
322
323 self.matcher = mp
324 self.picklist_delay = 50
325 self.selection_only = True
326
327 self.SetToolTip(_('Select a document.'))
328
329
334
335
340
341
398
399
400
401
409
410
411 from Gnumed.wxGladeWidgets import wxgEditDocumentTypesDlg
412
414 """A dialog showing a cEditDocumentTypesPnl."""
415
418
419
420 from Gnumed.wxGladeWidgets import wxgEditDocumentTypesPnl
421
423 """A panel grouping together fields to edit the list of document types."""
424
430
432 self._LCTRL_doc_type.set_columns([_('Type'), _('Translation'), _('User defined'), _('In use')])
433 self._LCTRL_doc_type.set_column_widths()
434
436 gmDispatcher.connect(signal = 'blobs.doc_type_mod_db', receiver = self._on_doc_type_mod_db)
437
440
442
443 self._LCTRL_doc_type.DeleteAllItems()
444
445 doc_types = gmDocuments.get_document_types()
446 pos = len(doc_types) + 1
447
448 for doc_type in doc_types:
449 row_num = self._LCTRL_doc_type.InsertItem(pos, label = doc_type['type'])
450 self._LCTRL_doc_type.SetItem(index = row_num, column = 1, label = doc_type['l10n_type'])
451 if doc_type['is_user_defined']:
452 self._LCTRL_doc_type.SetItem(index = row_num, column = 2, label = ' X ')
453 if doc_type['is_in_use']:
454 self._LCTRL_doc_type.SetItem(index = row_num, column = 3, label = ' X ')
455
456 if len(doc_types) > 0:
457 self._LCTRL_doc_type.set_data(data = doc_types)
458 self._LCTRL_doc_type.SetColumnWidth(0, wx.LIST_AUTOSIZE)
459 self._LCTRL_doc_type.SetColumnWidth(1, wx.LIST_AUTOSIZE)
460 self._LCTRL_doc_type.SetColumnWidth(2, wx.LIST_AUTOSIZE_USEHEADER)
461 self._LCTRL_doc_type.SetColumnWidth(3, wx.LIST_AUTOSIZE_USEHEADER)
462
463 self._TCTRL_type.SetValue('')
464 self._TCTRL_l10n_type.SetValue('')
465
466 self._BTN_set_translation.Enable(False)
467 self._BTN_delete.Enable(False)
468 self._BTN_add.Enable(False)
469 self._BTN_reassign.Enable(False)
470
471 self._LCTRL_doc_type.SetFocus()
472
473
474
476 doc_type = self._LCTRL_doc_type.get_selected_item_data()
477
478 self._TCTRL_type.SetValue(doc_type['type'])
479 self._TCTRL_l10n_type.SetValue(doc_type['l10n_type'])
480
481 self._BTN_set_translation.Enable(True)
482 self._BTN_delete.Enable(not bool(doc_type['is_in_use']))
483 self._BTN_add.Enable(False)
484 self._BTN_reassign.Enable(True)
485
486 return
487
489 self._BTN_set_translation.Enable(False)
490 self._BTN_delete.Enable(False)
491 self._BTN_reassign.Enable(False)
492
493 self._BTN_add.Enable(True)
494
495 return
496
503
520
530
562
563
565 """Let user select a document type."""
567
568 gmPhraseWheel.cPhraseWheel.__init__(self, *args, **kwargs)
569
570 mp = gmMatchProvider.cMatchProvider_SQL2 (
571 queries = [
572 """SELECT
573 data,
574 field_label,
575 list_label
576 FROM ((
577 SELECT
578 pk_doc_type AS data,
579 l10n_type AS field_label,
580 l10n_type AS list_label,
581 1 AS rank
582 FROM blobs.v_doc_type
583 WHERE
584 is_user_defined IS True
585 AND
586 l10n_type %(fragment_condition)s
587 ) UNION (
588 SELECT
589 pk_doc_type AS data,
590 l10n_type AS field_label,
591 l10n_type AS list_label,
592 2 AS rank
593 FROM blobs.v_doc_type
594 WHERE
595 is_user_defined IS False
596 AND
597 l10n_type %(fragment_condition)s
598 )) AS q1
599 ORDER BY q1.rank, q1.list_label"""]
600 )
601 mp.setThresholds(2, 4, 6)
602
603 self.matcher = mp
604 self.picklist_delay = 50
605
606 self.SetToolTip(_('Select the document type.'))
607
609
610 doc_type = self.GetValue().strip()
611 if doc_type == '':
612 gmDispatcher.send(signal = 'statustext', msg = _('Cannot create document type without name.'), beep = True)
613 _log.debug('cannot create document type without name')
614 return
615
616 pk = gmDocuments.create_document_type(doc_type)['pk_doc_type']
617 if pk is None:
618 self.data = {}
619 else:
620 self.SetText (
621 value = doc_type,
622 data = pk
623 )
624
625
626
627
638
639
642
643
644 from Gnumed.wxGladeWidgets import wxgReviewDocPartDlg
645
648 """Support parts and docs now.
649 """
650 part = kwds['part']
651 del kwds['part']
652 wxgReviewDocPartDlg.wxgReviewDocPartDlg.__init__(self, *args, **kwds)
653
654 if isinstance(part, gmDocuments.cDocumentPart):
655 self.__part = part
656 self.__doc = self.__part.containing_document
657 self.__reviewing_doc = False
658 elif isinstance(part, gmDocuments.cDocument):
659 self.__doc = part
660 if len(self.__doc.parts) == 0:
661 self.__part = None
662 else:
663 self.__part = self.__doc.parts[0]
664 self.__reviewing_doc = True
665 else:
666 raise ValueError('<part> must be gmDocuments.cDocument or gmDocuments.cDocumentPart instance, got <%s>' % type(part))
667
668 self.__init_ui_data()
669
670
671
672
674
675
676 self._PhWheel_episode.SetText('%s ' % self.__doc['episode'], self.__doc['pk_episode'])
677 self._PhWheel_doc_type.SetText(value = self.__doc['l10n_type'], data = self.__doc['pk_type'])
678 self._PhWheel_doc_type.add_callback_on_set_focus(self._on_doc_type_gets_focus)
679 self._PhWheel_doc_type.add_callback_on_lose_focus(self._on_doc_type_loses_focus)
680
681 if self.__reviewing_doc:
682 self._PRW_doc_comment.SetText(gmTools.coalesce(self.__doc['comment'], ''))
683 self._PRW_doc_comment.set_context(context = 'pk_doc_type', val = self.__doc['pk_type'])
684 else:
685 self._PRW_doc_comment.SetText(gmTools.coalesce(self.__part['obj_comment'], ''))
686
687 if self.__doc['pk_org_unit'] is not None:
688 self._PRW_org.SetText(value = '%s @ %s' % (self.__doc['unit'], self.__doc['organization']), data = self.__doc['pk_org_unit'])
689
690 if self.__doc['unit_is_receiver']:
691 self._RBTN_org_is_receiver.Value = True
692 else:
693 self._RBTN_org_is_source.Value = True
694
695 if self.__reviewing_doc:
696 self._PRW_org.Enable()
697 else:
698 self._PRW_org.Disable()
699
700 if self.__doc['pk_hospital_stay'] is not None:
701 self._PRW_hospital_stay.SetText(data = self.__doc['pk_hospital_stay'])
702
703 fts = gmDateTime.cFuzzyTimestamp(timestamp = self.__doc['clin_when'])
704 self._PhWheel_doc_date.SetText(fts.strftime('%Y-%m-%d'), fts)
705 self._TCTRL_reference.SetValue(gmTools.coalesce(self.__doc['ext_ref'], ''))
706 if self.__reviewing_doc:
707 self._TCTRL_filename.Enable(False)
708 self._SPINCTRL_seq_idx.Enable(False)
709 else:
710 self._TCTRL_filename.SetValue(gmTools.coalesce(self.__part['filename'], ''))
711 self._SPINCTRL_seq_idx.SetValue(gmTools.coalesce(self.__part['seq_idx'], 0))
712
713 self._LCTRL_existing_reviews.InsertColumn(0, _('who'))
714 self._LCTRL_existing_reviews.InsertColumn(1, _('when'))
715 self._LCTRL_existing_reviews.InsertColumn(2, _('+/-'))
716 self._LCTRL_existing_reviews.InsertColumn(3, _('!'))
717 self._LCTRL_existing_reviews.InsertColumn(4, _('comment'))
718
719 self.__reload_existing_reviews()
720
721 if self._LCTRL_existing_reviews.GetItemCount() > 0:
722 self._LCTRL_existing_reviews.SetColumnWidth(0, wx.LIST_AUTOSIZE)
723 self._LCTRL_existing_reviews.SetColumnWidth(1, wx.LIST_AUTOSIZE)
724 self._LCTRL_existing_reviews.SetColumnWidth(2, wx.LIST_AUTOSIZE_USEHEADER)
725 self._LCTRL_existing_reviews.SetColumnWidth(3, wx.LIST_AUTOSIZE_USEHEADER)
726 self._LCTRL_existing_reviews.SetColumnWidth(4, wx.LIST_AUTOSIZE)
727
728 if self.__part is None:
729 self._ChBOX_review.SetValue(False)
730 self._ChBOX_review.Enable(False)
731 self._ChBOX_abnormal.Enable(False)
732 self._ChBOX_relevant.Enable(False)
733 self._ChBOX_sign_all_pages.Enable(False)
734 else:
735 me = gmStaff.gmCurrentProvider()
736 if self.__part['pk_intended_reviewer'] == me['pk_staff']:
737 msg = _('(you are the primary reviewer)')
738 else:
739 other = gmStaff.cStaff(aPK_obj = self.__part['pk_intended_reviewer'])
740 msg = _('(someone else is the intended reviewer: %s)') % other['short_alias']
741 self._TCTRL_responsible.SetValue(msg)
742
743 if self.__part['reviewed_by_you']:
744 revs = self.__part.get_reviews()
745 for rev in revs:
746 if rev['is_your_review']:
747 self._ChBOX_abnormal.SetValue(bool(rev[2]))
748 self._ChBOX_relevant.SetValue(bool(rev[3]))
749 break
750
751 self._ChBOX_sign_all_pages.SetValue(self.__reviewing_doc)
752
753 return True
754
755
757 self._LCTRL_existing_reviews.DeleteAllItems()
758 if self.__part is None:
759 return True
760 revs = self.__part.get_reviews()
761 if len(revs) == 0:
762 return True
763
764 review_by_responsible_doc = None
765 reviews_by_others = []
766 for rev in revs:
767 if rev['is_review_by_responsible_reviewer'] and not rev['is_your_review']:
768 review_by_responsible_doc = rev
769 if not (rev['is_review_by_responsible_reviewer'] or rev['is_your_review']):
770 reviews_by_others.append(rev)
771
772 if review_by_responsible_doc is not None:
773 row_num = self._LCTRL_existing_reviews.InsertItem(sys.maxsize, label=review_by_responsible_doc[0])
774 self._LCTRL_existing_reviews.SetItemTextColour(row_num, wx.BLUE)
775 self._LCTRL_existing_reviews.SetItem(index = row_num, column=0, label=review_by_responsible_doc[0])
776 self._LCTRL_existing_reviews.SetItem(index = row_num, column=1, label=review_by_responsible_doc[1].strftime('%x %H:%M'))
777 if review_by_responsible_doc['is_technically_abnormal']:
778 self._LCTRL_existing_reviews.SetItem(index = row_num, column=2, label='X')
779 if review_by_responsible_doc['clinically_relevant']:
780 self._LCTRL_existing_reviews.SetItem(index = row_num, column=3, label='X')
781 self._LCTRL_existing_reviews.SetItem(index = row_num, column=4, label=review_by_responsible_doc[6])
782 row_num += 1
783 for rev in reviews_by_others:
784 row_num = self._LCTRL_existing_reviews.InsertItem(sys.maxsize, label=rev[0])
785 self._LCTRL_existing_reviews.SetItem(index = row_num, column=0, label=rev[0])
786 self._LCTRL_existing_reviews.SetItem(index = row_num, column=1, label=rev[1].strftime('%x %H:%M'))
787 if rev['is_technically_abnormal']:
788 self._LCTRL_existing_reviews.SetItem(index = row_num, column=2, label='X')
789 if rev['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=rev[6])
792 return True
793
794
795
796
893
894
896 state = self._ChBOX_review.GetValue()
897 self._ChBOX_abnormal.Enable(enable = state)
898 self._ChBOX_relevant.Enable(enable = state)
899 self._ChBOX_responsible.Enable(enable = state)
900
901
903 """Per Jim: Changing the doc type happens a lot more often
904 then correcting spelling, hence select-all on getting focus.
905 """
906 self._PhWheel_doc_type.SetSelection(-1, -1)
907
908
910 pk_doc_type = self._PhWheel_doc_type.GetData()
911 if pk_doc_type is None:
912 self._PRW_doc_comment.unset_context(context = 'pk_doc_type')
913 else:
914 self._PRW_doc_comment.set_context(context = 'pk_doc_type', val = pk_doc_type)
915 return True
916
917
919
920 _log.debug('acquiring images from [%s]', device)
921
922
923
924 from Gnumed.pycommon import gmScanBackend
925 try:
926 fnames = gmScanBackend.acquire_pages_into_files (
927 device = device,
928 delay = 5,
929 calling_window = calling_window
930 )
931 except OSError:
932 _log.exception('problem acquiring image from source')
933 gmGuiHelpers.gm_show_error (
934 aMessage = _(
935 'No images could be acquired from the source.\n\n'
936 'This may mean the scanner driver is not properly installed.\n\n'
937 'On Windows you must install the TWAIN Python module\n'
938 'while on Linux and MacOSX it is recommended to install\n'
939 'the XSane package.'
940 ),
941 aTitle = _('Acquiring images')
942 )
943 return None
944
945 _log.debug('acquired %s images', len(fnames))
946
947 return fnames
948
949
950 from Gnumed.wxGladeWidgets import wxgScanIdxPnl
951
952 -class cScanIdxDocsPnl(wxgScanIdxPnl.wxgScanIdxPnl, gmPlugin.cPatientChange_PluginMixin):
953
973
974
975
976
978 pat = gmPerson.gmCurrentPatient()
979 if not pat.connected:
980 gmDispatcher.send(signal='statustext', msg=_('Cannot accept new documents. No active patient.'))
981 return
982
983
984 real_filenames = []
985 for pathname in filenames:
986 try:
987 files = os.listdir(pathname)
988 source = _('directory dropped on client')
989 gmDispatcher.send(signal = 'statustext', msg = _('Extracting files from folder [%s] ...') % pathname)
990 for filename in files:
991 fullname = os.path.join(pathname, filename)
992 if not os.path.isfile(fullname):
993 continue
994 real_filenames.append(fullname)
995 except OSError:
996 source = _('file dropped on client')
997 real_filenames.append(pathname)
998
999 self.add_parts_from_files(real_filenames, source)
1000
1001
1004
1005
1006
1007
1011
1012
1013 - def _post_patient_selection(self, **kwds):
1014 self.__init_ui_data()
1015
1016
1017
1018
1055
1056
1065
1066
1068 title = _('saving document')
1069
1070 if self._LCTRL_doc_pages.ItemCount == 0:
1071 dbcfg = gmCfg.cCfgSQL()
1072 allow_empty = bool(dbcfg.get2 (
1073 option = 'horstspace.scan_index.allow_partless_documents',
1074 workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace,
1075 bias = 'user',
1076 default = False
1077 ))
1078 if allow_empty:
1079 save_empty = gmGuiHelpers.gm_show_question (
1080 aMessage = _('No parts to save. Really save an empty document as a reference ?'),
1081 aTitle = title
1082 )
1083 if not save_empty:
1084 return False
1085 else:
1086 gmGuiHelpers.gm_show_error (
1087 aMessage = _('No parts to save. Aquire some parts first.'),
1088 aTitle = title
1089 )
1090 return False
1091
1092 if self._LCTRL_doc_pages.ItemCount > 0:
1093 for fname in [ data[0] for data in self._LCTRL_doc_pages.data ]:
1094 try:
1095 open(fname, 'rb').close()
1096 except OSError:
1097 _log.exception('cannot access [%s]', fname)
1098 gmGuiHelpers.gm_show_error(title = title, error = _('Cannot access document part file:\n\n %s') % fname)
1099 return False
1100
1101 doc_type_pk = self._PhWheel_doc_type.GetData(can_create = True)
1102 if doc_type_pk is None:
1103 gmGuiHelpers.gm_show_error (
1104 aMessage = _('No document type applied. Choose a document type'),
1105 aTitle = title
1106 )
1107 return False
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117 if self._PhWheel_episode.GetValue().strip() == '':
1118 gmGuiHelpers.gm_show_error (
1119 aMessage = _('You must select an episode to save this document under.'),
1120 aTitle = title
1121 )
1122 return False
1123
1124 if self._PhWheel_reviewer.GetData() is None:
1125 gmGuiHelpers.gm_show_error (
1126 aMessage = _('You need to select from the list of staff members the doctor who is intended to sign the document.'),
1127 aTitle = title
1128 )
1129 return False
1130
1131 if self._PhWheel_doc_date.is_valid_timestamp(empty_is_valid = True) is False:
1132 gmGuiHelpers.gm_show_error (
1133 aMessage = _('Invalid date of generation.'),
1134 aTitle = title
1135 )
1136 return False
1137
1138 return True
1139
1140
1142
1143 if not reconfigure:
1144 dbcfg = gmCfg.cCfgSQL()
1145 device = dbcfg.get2 (
1146 option = 'external.xsane.default_device',
1147 workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace,
1148 bias = 'workplace',
1149 default = ''
1150 )
1151 if device.strip() == '':
1152 device = None
1153 if device is not None:
1154 return device
1155
1156 try:
1157 devices = self.scan_module.get_devices()
1158 except Exception:
1159 _log.exception('cannot retrieve list of image sources')
1160 gmDispatcher.send(signal = 'statustext', msg = _('There is no scanner support installed on this machine.'))
1161 return None
1162
1163 if devices is None:
1164
1165
1166 return None
1167
1168 if len(devices) == 0:
1169 gmDispatcher.send(signal = 'statustext', msg = _('Cannot find an active scanner.'))
1170 return None
1171
1172
1173
1174
1175
1176 device = gmListWidgets.get_choices_from_list (
1177 parent = self,
1178 msg = _('Select an image capture device'),
1179 caption = _('device selection'),
1180 choices = [ '%s (%s)' % (d[2], d[0]) for d in devices ],
1181 columns = [_('Device')],
1182 data = devices,
1183 single_selection = True
1184 )
1185 if device is None:
1186 return None
1187
1188
1189 return device[0]
1190
1191
1192
1193
1195
1196 chosen_device = self.get_device_to_use()
1197
1198
1199
1200 try:
1201 fnames = self.scan_module.acquire_pages_into_files (
1202 device = chosen_device,
1203 delay = 5,
1204 calling_window = self
1205 )
1206 except OSError:
1207 _log.exception('problem acquiring image from source')
1208 gmGuiHelpers.gm_show_error (
1209 aMessage = _(
1210 'No pages could be acquired from the source.\n\n'
1211 'This may mean the scanner driver is not properly installed.\n\n'
1212 'On Windows you must install the TWAIN Python module\n'
1213 'while on Linux and MacOSX it is recommended to install\n'
1214 'the XSane package.'
1215 ),
1216 aTitle = _('acquiring page')
1217 )
1218 return None
1219
1220 if len(fnames) == 0:
1221 return True
1222
1223 self.add_parts_from_files(fnames, _('captured by imaging device'))
1224 return True
1225
1226
1228 dlg = wx.FileDialog (
1229 parent = None,
1230 message = _('Choose a file'),
1231 defaultDir = os.path.expanduser(os.path.join('~', 'gnumed')),
1232 defaultFile = '',
1233 wildcard = "%s (*)|*|TIFFs (*.tif)|*.tif|JPEGs (*.jpg)|*.jpg|%s (*.*)|*.*" % (_('all files'), _('all files (Win)')),
1234 style = wx.FD_OPEN | wx.FD_FILE_MUST_EXIST | wx.FD_MULTIPLE
1235 )
1236 result = dlg.ShowModal()
1237 files = dlg.GetPaths()
1238 if result == wx.ID_CANCEL:
1239 dlg.DestroyLater()
1240 return
1241
1242 self.add_parts_from_files(files, _('picked from storage media'))
1243
1244
1253
1254
1256
1257
1258 if self._LCTRL_doc_pages.ItemCount == 0:
1259 return
1260
1261
1262 if self._LCTRL_doc_pages.ItemCount == 1:
1263 page_fnames = [ self._LCTRL_doc_pages.get_item_data(0)[0] ]
1264 else:
1265
1266 page_fnames = [ data[0] for data in self._LCTRL_doc_pages.selected_item_data ]
1267 if len(page_fnames) == 0:
1268 gmDispatcher.send(signal = 'statustext', msg = _('No part selected for viewing.'), beep = True)
1269 return
1270
1271 for page_fname in page_fnames:
1272 (success, msg) = gmMimeLib.call_viewer_on_file(page_fname)
1273 if not success:
1274 gmGuiHelpers.gm_show_warning (
1275 aMessage = _('Cannot display document part:\n%s') % msg,
1276 aTitle = _('displaying part')
1277 )
1278
1279
1281
1282 if len(self._LCTRL_doc_pages.selected_items) == 0:
1283 gmDispatcher.send(signal = 'statustext', msg = _('No part selected for removal.'), beep = True)
1284 return
1285
1286 sel_idx = self._LCTRL_doc_pages.GetFirstSelected()
1287 rows = self._LCTRL_doc_pages.string_items
1288 data = self._LCTRL_doc_pages.data
1289 del rows[sel_idx]
1290 del data[sel_idx]
1291 self._LCTRL_doc_pages.string_items = rows
1292 self._LCTRL_doc_pages.data = data
1293 self._LCTRL_doc_pages.set_column_widths()
1294 self._TCTRL_metadata.SetValue('')
1295
1296
1298
1299 if not self.__valid_for_save():
1300 return False
1301
1302
1303 cfg = gmCfg.cCfgSQL()
1304 generate_uuid = bool (
1305 cfg.get2 (
1306 option = 'horstspace.scan_index.generate_doc_uuid',
1307 workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace,
1308 bias = 'user',
1309 default = False
1310 )
1311 )
1312 if generate_uuid:
1313 ext_ref = gmDocuments.get_ext_ref()
1314 else:
1315 ext_ref = None
1316
1317
1318 date = self._PhWheel_doc_date.GetData()
1319 if date is not None:
1320 date = date.get_pydt()
1321 new_doc = save_files_as_new_document (
1322 parent = self,
1323 filenames = [ data[0] for data in self._LCTRL_doc_pages.data ],
1324 document_type = self._PhWheel_doc_type.GetValue().strip(),
1325 pk_document_type = self._PhWheel_doc_type.GetData(),
1326 unlock_patient = False,
1327 episode = self._PhWheel_episode.GetData(can_create = True, is_open = True, as_instance = True),
1328 review_as_normal = False,
1329 reference = ext_ref,
1330 pk_org_unit = self._PhWheel_source.GetData(),
1331 date_generated = date,
1332 comment = self._PRW_doc_comment.GetLineText(0).strip(),
1333 reviewer = self._PhWheel_reviewer.GetData()
1334 )
1335 if new_doc is None:
1336 return False
1337
1338 if self._RBTN_org_is_receiver.Value is True:
1339 new_doc['unit_is_receiver'] = True
1340 new_doc.save()
1341
1342
1343 description = self._TBOX_description.GetValue().strip()
1344 if description != '':
1345 if not new_doc.add_description(description):
1346 wx.EndBusyCursor()
1347 gmGuiHelpers.gm_show_error (
1348 aMessage = _('Cannot add document description.'),
1349 aTitle = _('saving document')
1350 )
1351 return False
1352
1353
1354 if self._ChBOX_reviewed.GetValue():
1355 if not new_doc.set_reviewed (
1356 technically_abnormal = self._ChBOX_abnormal.GetValue(),
1357 clinically_relevant = self._ChBOX_relevant.GetValue()
1358 ):
1359 msg = _('Error setting "reviewed" status of new document.')
1360
1361 self.__init_ui_data()
1362
1363 gmHooks.run_hook_script(hook = 'after_new_doc_created')
1364
1365 return True
1366
1367
1369 self.__init_ui_data()
1370
1371
1373 self._ChBOX_abnormal.Enable(enable = self._ChBOX_reviewed.GetValue())
1374 self._ChBOX_relevant.Enable(enable = self._ChBOX_reviewed.GetValue())
1375
1376
1378 pk_doc_type = self._PhWheel_doc_type.GetData()
1379 if pk_doc_type is None:
1380 self._PRW_doc_comment.unset_context(context = 'pk_doc_type')
1381 else:
1382 self._PRW_doc_comment.set_context(context = 'pk_doc_type', val = pk_doc_type)
1383 return True
1384
1385
1387 status, description = result
1388 fname, source = self._LCTRL_doc_pages.get_selected_item_data(only_one = True)
1389 txt = _(
1390 'Source: %s\n'
1391 'File: %s\n'
1392 '\n'
1393 '%s'
1394 ) % (
1395 source,
1396 fname,
1397 description
1398 )
1399 wx.CallAfter(self._TCTRL_metadata.SetValue, txt)
1400
1401
1407
1408
1410
1411 if parent is None:
1412 parent = wx.GetApp().GetTopWindow()
1413
1414
1415 if part['size'] == 0:
1416 _log.debug('cannot display part [%s] - 0 bytes', part['pk_obj'])
1417 gmGuiHelpers.gm_show_error (
1418 aMessage = _('Document part does not seem to exist in database !'),
1419 aTitle = _('showing document')
1420 )
1421 return None
1422
1423 wx.BeginBusyCursor()
1424 cfg = gmCfg.cCfgSQL()
1425
1426
1427 chunksize = int(
1428 cfg.get2 (
1429 option = "horstspace.blob_export_chunk_size",
1430 workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace,
1431 bias = 'workplace',
1432 default = 2048
1433 ))
1434
1435
1436 block_during_view = bool( cfg.get2 (
1437 option = 'horstspace.document_viewer.block_during_view',
1438 workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace,
1439 bias = 'user',
1440 default = None
1441 ))
1442
1443 wx.EndBusyCursor()
1444
1445
1446 successful, msg = part.display_via_mime (
1447 chunksize = chunksize,
1448 block = block_during_view
1449 )
1450 if not successful:
1451 gmGuiHelpers.gm_show_error (
1452 aMessage = _('Cannot display document part:\n%s') % msg,
1453 aTitle = _('showing document')
1454 )
1455 return None
1456
1457
1458
1459
1460
1461
1462
1463 review_after_display = int(cfg.get2 (
1464 option = 'horstspace.document_viewer.review_after_display',
1465 workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace,
1466 bias = 'user',
1467 default = 3
1468 ))
1469 if review_after_display == 1:
1470 review_document_part(parent = parent, part = part)
1471 elif review_after_display == 2:
1472 review_by_me = [ rev for rev in part.get_reviews() if rev['is_your_review'] ]
1473 if len(review_by_me) == 0:
1474 review_document_part(parent = parent, part = part)
1475 elif review_after_display == 3:
1476 if len(part.get_reviews()) == 0:
1477 review_document_part(parent = parent, part = part)
1478 elif review_after_display == 4:
1479 reviewed_by_responsible = [ rev for rev in part.get_reviews() if rev['is_review_by_responsible_reviewer'] ]
1480 if len(reviewed_by_responsible) == 0:
1481 review_document_part(parent = parent, part = part)
1482
1483 return True
1484
1485
1486 -def manage_documents(parent=None, msg=None, single_selection=True, pk_types=None, pk_episodes=None):
1496
1497
1498
1499 def delete(document):
1500 return
1501
1502
1503
1504
1505
1506
1507
1508 def refresh(lctrl):
1509 docs = pat.document_folder.get_documents(pk_types = pk_types, pk_episodes = pk_episodes)
1510 items = [ [
1511 gmDateTime.pydt_strftime(d['clin_when'], '%Y %b %d', accuracy = gmDateTime.acc_days),
1512 d['l10n_type'],
1513 gmTools.coalesce(d['comment'], ''),
1514 gmTools.coalesce(d['ext_ref'], ''),
1515 d['pk_doc']
1516 ] for d in docs ]
1517 lctrl.set_string_items(items)
1518 lctrl.set_data(docs)
1519
1520
1521 def show_doc(doc):
1522 if doc is None:
1523 return
1524 for fname in doc.save_parts_to_files():
1525 gmMimeLib.call_viewer_on_file(aFile = fname, block = False)
1526
1527
1528 return gmListWidgets.get_choices_from_list (
1529 parent = parent,
1530 caption = _('Patient document list'),
1531 columns = [_('Generated'), _('Type'), _('Comment'), _('Ref #'), '#'],
1532 single_selection = single_selection,
1533
1534
1535
1536 refresh_callback = refresh,
1537 left_extra_button = (_('Show'), _('Show all parts of this document in external viewer.'), show_doc)
1538 )
1539
1540
1541 from Gnumed.wxGladeWidgets import wxgSelectablySortedDocTreePnl
1542
1544 """A panel with a document tree which can be sorted.
1545
1546 On the right there's a list ctrl showing
1547 details of the selected node.
1548 """
1549 - def __init__(self, parent, id, *args, **kwds):
1559
1560
1561
1562
1567
1568
1573
1574
1579
1580
1585
1586
1591
1592
1597
1598
1599
1600 - def _update_details(self, issue=None, episode=None, org_unit=None, document=None, part=None):
1601
1602 self._LCTRL_details.set_string_items([])
1603 self._TCTRL_metainfo.Value = ''
1604
1605 if document is None:
1606 if part is not None:
1607 document = part.document
1608
1609 if issue is None:
1610 if episode is not None:
1611 issue = episode.health_issue
1612
1613 items = []
1614 items.extend(self.__process_issue(issue))
1615 items.extend(self.__process_episode(episode))
1616 items.extend(self.__process_org_unit(org_unit))
1617 items.extend(self.__process_document(document))
1618
1619 items.extend(self.__process_document_part(part))
1620 self._LCTRL_details.set_string_items(items)
1621 self._LCTRL_details.set_column_widths()
1622 self._LCTRL_details.set_resize_column(1)
1623
1624
1625
1626
1628 if self.__pk_curr_pat == pk_patient:
1629 return
1630 self.__metainfo4parts = {}
1631
1632
1640
1641
1653
1654
1656 if issue is None:
1657 return []
1658
1659 self.__check_cache_validity(issue['pk_patient'])
1660 self.__pk_curr_doc_part = None
1661
1662 items = []
1663 items.append([_('Health issue'), '%s%s [#%s]' % (
1664 issue['description'],
1665 gmTools.coalesce (
1666 value2test = issue['laterality'],
1667 return_instead = '',
1668 template4value = ' (%s)',
1669 none_equivalents = [None, '', '?']
1670 ),
1671 issue['pk_health_issue']
1672 )])
1673 items.append([_('Status'), '%s, %s %s' % (
1674 gmTools.bool2subst(issue['is_active'], _('active'), _('inactive')),
1675 gmTools.bool2subst(issue['clinically_relevant'], _('clinically relevant'), _('not clinically relevant')),
1676 issue.diagnostic_certainty_description
1677 )])
1678 items.append([_('Confidential'), issue['is_confidential']])
1679 items.append([_('Age noted'), issue.age_noted_human_readable()])
1680 return items
1681
1682
1684 if episode is None:
1685 return []
1686
1687 self.__check_cache_validity(episode['pk_patient'])
1688 self.__pk_curr_doc_part = None
1689
1690 items = []
1691 items.append([_('Episode'), '%s [#%s]' % (
1692 episode['description'],
1693 episode['pk_episode']
1694 )])
1695 items.append([_('Status'), '%s %s' % (
1696 gmTools.bool2subst(episode['episode_open'], _('active'), _('finished')),
1697 episode.diagnostic_certainty_description
1698 )])
1699 items.append([_('Health issue'), gmTools.coalesce(episode['health_issue'], '')])
1700 return items
1701
1702
1704 if org_unit is None:
1705 return []
1706
1707
1708
1709 self.__pk_curr_doc_part = None
1710 self._TCTRL_metainfo.Value = ''
1711
1712 items = []
1713 items.append([_('Organization'), '%s (%s) [#%s]' % (
1714 org_unit['organization'],
1715 org_unit['l10n_organization_category'],
1716 org_unit['pk_org']
1717 )])
1718 items.append([_('Department'), '%s%s [#%s]' % (
1719 org_unit['unit'],
1720 gmTools.coalesce(org_unit['l10n_unit_category'], '', ' (%s)'),
1721 org_unit['pk_org_unit']
1722 )])
1723 adr = org_unit.address
1724 if adr is not None:
1725 lines = adr.format()
1726 items.append([lines[0], lines[1]])
1727 for line in lines[2:]:
1728 items.append(['', line])
1729 for comm in org_unit.comm_channels:
1730 items.append([comm['l10n_comm_type'], '%s%s' % (
1731 comm['url'],
1732 gmTools.bool2subst(comm['is_confidential'], _(' (confidential)'), '', '')
1733 )])
1734 return items
1735
1736
1738 if document is None:
1739 return []
1740
1741 self.__check_cache_validity(document['pk_patient'])
1742 self.__pk_curr_doc_part = None
1743
1744 items = []
1745 items.append([_('Document'), '%s [#%s]' % (document['l10n_type'], document['pk_doc'])])
1746 items.append([_('Generated'), gmDateTime.pydt_strftime(document['clin_when'], '%Y %b %d')])
1747 items.append([_('Health issue'), gmTools.coalesce(document['health_issue'], '', '%%s [#%s]' % document['pk_health_issue'])])
1748 items.append([_('Episode'), '%s (%s) [#%s]' % (
1749 document['episode'],
1750 gmTools.bool2subst(document['episode_open'], _('open'), _('closed')),
1751 document['pk_episode']
1752 )])
1753 if document['pk_org_unit'] is not None:
1754 if document['unit_is_receiver']:
1755 header = _('Receiver')
1756 else:
1757 header = _('Sender')
1758 items.append([header, '%s @ %s' % (document['unit'], document['organization'])])
1759 if document['ext_ref'] is not None:
1760 items.append([_('Reference'), document['ext_ref']])
1761 if document['comment'] is not None:
1762 items.append([_('Comment'), ' / '.join(document['comment'].split('\n'))])
1763 for proc in document.procedures:
1764 items.append([_('Procedure'), proc.format (
1765 left_margin = 0,
1766 include_episode = False,
1767 include_codes = False,
1768 include_address = False,
1769 include_comm = False,
1770 include_doc = False
1771 )])
1772 stay = document.hospital_stay
1773 if stay is not None:
1774 items.append([_('Hospital stay'), stay.format(include_episode = False)])
1775 for bill in document.bills:
1776 items.append([_('Bill'), bill.format (
1777 include_receiver = False,
1778 include_doc = False
1779 )])
1780 items.append([_('Modified'), gmDateTime.pydt_strftime(document['modified_when'], '%Y %b %d')])
1781 items.append([_('... by'), document['modified_by']])
1782 items.append([_('# encounter'), document['pk_encounter']])
1783 return items
1784
1785
1787 if document_part is None:
1788 return []
1789
1790 self.__check_cache_validity(document_part['pk_patient'])
1791 self.__pk_curr_doc_part = document_part['pk_obj']
1792 self.__update_metainfo(document_part['pk_obj'], document_part)
1793 items = []
1794 items.append(['', ''])
1795 if document_part['seq_idx'] is None:
1796 items.append([_('Part'), '#%s' % document_part['pk_obj']])
1797 else:
1798 items.append([_('Part'), '%s [#%s]' % (document_part['seq_idx'], document_part['pk_obj'])])
1799 if document_part['obj_comment'] is not None:
1800 items.append([_('Comment'), document_part['obj_comment']])
1801 if document_part['filename'] is not None:
1802 items.append([_('Filename'), document_part['filename']])
1803 items.append([_('Data size'), gmTools.size2str(document_part['size'])])
1804 review_parts = []
1805 if document_part['reviewed_by_you']:
1806 review_parts.append(_('by you'))
1807 if document_part['reviewed_by_intended_reviewer']:
1808 review_parts.append(_('by intended reviewer'))
1809 review = ', '.join(review_parts)
1810 if review == '':
1811 review = gmTools.u_diameter
1812 items.append([_('Reviewed'), review])
1813
1814 return items
1815
1816
1817 -class cDocTree(wx.TreeCtrl, gmRegetMixin.cRegetOnPaintMixin, treemixin.ExpansionState):
1818 """This wx.TreeCtrl derivative displays a tree view of stored medical documents.
1819
1820 It listens to document and patient changes and updates itself accordingly.
1821
1822 This acts on the current patient.
1823 """
1824 _sort_modes = ['age', 'review', 'episode', 'type', 'issue', 'org']
1825 _root_node_labels = None
1826
1827
1828 - def __init__(self, parent, id, *args, **kwds):
1829 """Set up our specialised tree.
1830 """
1831 kwds['style'] = wx.TR_NO_BUTTONS | wx.NO_BORDER | wx.TR_SINGLE
1832 wx.TreeCtrl.__init__(self, parent, id, *args, **kwds)
1833
1834 gmRegetMixin.cRegetOnPaintMixin.__init__(self)
1835
1836 tmp = _('available documents (%s)')
1837 unsigned = _('unsigned (%s) on top') % '\u270D'
1838 cDocTree._root_node_labels = {
1839 'age': tmp % _('most recent on top'),
1840 'review': tmp % unsigned,
1841 'episode': tmp % _('sorted by episode'),
1842 'issue': tmp % _('sorted by health issue'),
1843 'type': tmp % _('sorted by type'),
1844 'org': tmp % _('sorted by organization')
1845 }
1846
1847 self.root = None
1848 self.__sort_mode = 'age'
1849
1850 self.__expanded_nodes = None
1851 self.__show_details_callback = None
1852
1853 self.__build_context_menus()
1854 self.__register_interests()
1855 self._schedule_data_reget()
1856
1857
1858
1859
1861
1862 node = self.GetSelection()
1863 node_data = self.GetItemData(node)
1864
1865 if not isinstance(node_data, gmDocuments.cDocumentPart):
1866 return True
1867
1868 self.__display_part(part = node_data)
1869 return True
1870
1871
1872
1873
1875 return self.__sort_mode
1876
1895
1896 sort_mode = property(_get_sort_mode, _set_sort_mode)
1897
1898
1900 if callback is not None:
1901 if not callable(callback):
1902 raise ValueError('<%s> is not callable')
1903 self.__show_details_callback = callback
1904
1905 show_details_callback = property(lambda x:x, _set_show_details_callback)
1906
1907
1908
1909
1911 curr_pat = gmPerson.gmCurrentPatient()
1912 if not curr_pat.connected:
1913 gmDispatcher.send(signal = 'statustext', msg = _('Cannot load documents. No active patient.'))
1914 return False
1915
1916 if not self.__populate_tree():
1917 return False
1918
1919 return True
1920
1921
1922
1923
1925 self.Bind(wx.EVT_TREE_SEL_CHANGED, self._on_tree_item_selected)
1926 self.Bind(wx.EVT_TREE_ITEM_ACTIVATED, self._on_activate)
1927 self.Bind(wx.EVT_TREE_ITEM_MENU, self._on_tree_item_context_menu)
1928 self.Bind(wx.EVT_TREE_ITEM_GETTOOLTIP, self._on_tree_item_gettooltip)
1929
1930 gmDispatcher.connect(signal = 'pre_patient_unselection', receiver = self._on_pre_patient_unselection)
1931 gmDispatcher.connect(signal = 'post_patient_selection', receiver = self._on_post_patient_selection)
1932 gmDispatcher.connect(signal = 'blobs.doc_med_mod_db', receiver = self._on_doc_mod_db)
1933 gmDispatcher.connect(signal = 'blobs.doc_obj_mod_db', receiver = self._on_doc_page_mod_db)
1934
1935
1937
1938
1939 self.__part_context_menu = wx.Menu(title = _('Part Actions:'))
1940
1941 item = self.__part_context_menu.Append(-1, _('Display part'))
1942 self.Bind(wx.EVT_MENU, self.__display_curr_part, item)
1943 item = self.__part_context_menu.Append(-1, _('%s Sign/Edit properties') % '\u270D')
1944 self.Bind(wx.EVT_MENU, self.__review_curr_part, item)
1945
1946 self.__part_context_menu.AppendSeparator()
1947
1948 item = self.__part_context_menu.Append(-1, _('Delete part'))
1949 self.Bind(wx.EVT_MENU, self.__delete_part, item, item)
1950 item = self.__part_context_menu.Append(-1, _('Move part'))
1951 self.Bind(wx.EVT_MENU, self.__move_part, item)
1952 item = self.__part_context_menu.Append(-1, _('Print part'))
1953 self.Bind(wx.EVT_MENU, self.__print_part, item)
1954 item = self.__part_context_menu.Append(-1, _('Fax part'))
1955 self.Bind(wx.EVT_MENU, self.__fax_part, item)
1956 item = self.__part_context_menu.Append(-1, _('Mail part'))
1957 self.Bind(wx.EVT_MENU, self.__mail_part, item)
1958 item = self.__part_context_menu.Append(-1, _('Save part to disk'))
1959 self.Bind(wx.EVT_MENU, self.__save_part_to_disk, item)
1960
1961 self.__part_context_menu.AppendSeparator()
1962
1963
1964 self.__doc_context_menu = wx.Menu(title = _('Document Actions:'))
1965
1966 item = self.__doc_context_menu.Append(-1, _('%s Sign/Edit properties') % '\u270D')
1967 self.Bind(wx.EVT_MENU, self.__review_curr_part, item)
1968 item = self.__doc_context_menu.Append(-1, _('Delete document'))
1969 self.Bind(wx.EVT_MENU, self.__delete_document, item)
1970
1971 self.__doc_context_menu.AppendSeparator()
1972
1973 item = self.__doc_context_menu.Append(-1, _('Add parts'))
1974 self.Bind(wx.EVT_MENU, self.__add_part, item)
1975 item = self.__doc_context_menu.Append(-1, _('Add part from clipboard'))
1976 self.Bind(wx.EVT_MENU, self.__add_part_from_clipboard, item, item)
1977 item = self.__doc_context_menu.Append(-1, _('Print all parts'))
1978 self.Bind(wx.EVT_MENU, self.__print_doc, item)
1979 item = self.__doc_context_menu.Append(-1, _('Fax all parts'))
1980 self.Bind(wx.EVT_MENU, self.__fax_doc, item)
1981 item = self.__doc_context_menu.Append(-1, _('Mail all parts'))
1982 self.Bind(wx.EVT_MENU, self.__mail_doc, item)
1983 item = self.__doc_context_menu.Append(-1, _('Save all parts to disk'))
1984 self.Bind(wx.EVT_MENU, self.__save_doc_to_disk, item)
1985 item = self.__doc_context_menu.Append(-1, _('Copy all parts to export area'))
1986 self.Bind(wx.EVT_MENU, self.__copy_doc_to_export_area, item)
1987
1988 self.__doc_context_menu.AppendSeparator()
1989
1990 item = self.__doc_context_menu.Append(-1, _('Access external original'))
1991 self.Bind(wx.EVT_MENU, self.__access_external_original, item)
1992 item = self.__doc_context_menu.Append(-1, _('Edit corresponding encounter'))
1993 self.Bind(wx.EVT_MENU, self.__edit_encounter_details, item)
1994 item = self.__doc_context_menu.Append(-1, _('Select corresponding encounter'))
1995 self.Bind(wx.EVT_MENU, self.__select_encounter, item)
1996 item = self.__doc_context_menu.Append(-1, _('Manage descriptions'))
1997 self.Bind(wx.EVT_MENU, self.__manage_document_descriptions, item)
1998
1999
2000
2001
2002
2003
2004
2005
2006
2007
2008
2010
2011 wx.BeginBusyCursor()
2012
2013
2014 if self.root is not None:
2015 self.DeleteAllItems()
2016
2017
2018 self.root = self.AddRoot(cDocTree._root_node_labels[self.__sort_mode], -1, -1)
2019 self.SetItemData(self.root, None)
2020 self.SetItemHasChildren(self.root, False)
2021
2022
2023 curr_pat = gmPerson.gmCurrentPatient()
2024 docs_folder = curr_pat.get_document_folder()
2025 docs = docs_folder.get_documents()
2026
2027 if docs is None:
2028 gmGuiHelpers.gm_show_error (
2029 aMessage = _('Error searching documents.'),
2030 aTitle = _('loading document list')
2031 )
2032
2033 wx.EndBusyCursor()
2034 return True
2035
2036 if len(docs) == 0:
2037 wx.EndBusyCursor()
2038 return True
2039
2040
2041 self.SetItemHasChildren(self.root, True)
2042
2043
2044 intermediate_nodes = {}
2045 for doc in docs:
2046
2047 parts = doc.parts
2048
2049 if len(parts) == 0:
2050 no_parts = _('no parts')
2051 elif len(parts) == 1:
2052 no_parts = _('1 part')
2053 else:
2054 no_parts = _('%s parts') % len(parts)
2055
2056
2057 if self.__sort_mode == 'episode':
2058 intermediate_label = '%s%s' % (doc['episode'], gmTools.coalesce(doc['health_issue'], '', ' (%s)'))
2059 doc_label = _('%s%7s %s:%s (%s)') % (
2060 gmTools.bool2subst(doc.has_unreviewed_parts, gmTools.u_writing_hand, '', '?'),
2061 gmDateTime.pydt_strftime(doc['clin_when'], '%m/%Y'),
2062 doc['l10n_type'][:26],
2063 gmTools.coalesce(value2test = doc['comment'], return_instead = '', template4value = ' %s'),
2064 no_parts
2065 )
2066 if intermediate_label not in intermediate_nodes:
2067 intermediate_nodes[intermediate_label] = self.AppendItem(parent = self.root, text = intermediate_label)
2068 self.SetItemBold(intermediate_nodes[intermediate_label], bold = True)
2069 self.SetItemData(intermediate_nodes[intermediate_label], {'pk_episode': doc['pk_episode']})
2070 self.SetItemHasChildren(intermediate_nodes[intermediate_label], True)
2071 parent = intermediate_nodes[intermediate_label]
2072
2073 elif self.__sort_mode == 'type':
2074 intermediate_label = doc['l10n_type']
2075 doc_label = _('%s%7s (%s):%s') % (
2076 gmTools.bool2subst(doc.has_unreviewed_parts, gmTools.u_writing_hand, '', '?'),
2077 gmDateTime.pydt_strftime(doc['clin_when'], '%m/%Y'),
2078 no_parts,
2079 gmTools.coalesce(value2test = doc['comment'], return_instead = '', template4value = ' %s')
2080 )
2081 if intermediate_label not in intermediate_nodes:
2082 intermediate_nodes[intermediate_label] = self.AppendItem(parent = self.root, text = intermediate_label)
2083 self.SetItemBold(intermediate_nodes[intermediate_label], bold = True)
2084 self.SetItemData(intermediate_nodes[intermediate_label], None)
2085 self.SetItemHasChildren(intermediate_nodes[intermediate_label], True)
2086 parent = intermediate_nodes[intermediate_label]
2087
2088 elif self.__sort_mode == 'issue':
2089 if doc['health_issue'] is None:
2090 intermediate_label = _('%s (unattributed episode)') % doc['episode']
2091 else:
2092 intermediate_label = doc['health_issue']
2093 doc_label = _('%s%7s %s:%s (%s)') % (
2094 gmTools.bool2subst(doc.has_unreviewed_parts, gmTools.u_writing_hand, '', '?'),
2095 gmDateTime.pydt_strftime(doc['clin_when'], '%m/%Y'),
2096 doc['l10n_type'][:26],
2097 gmTools.coalesce(value2test = doc['comment'], return_instead = '', template4value = ' %s'),
2098 no_parts
2099 )
2100 if intermediate_label not in intermediate_nodes:
2101 intermediate_nodes[intermediate_label] = self.AppendItem(parent = self.root, text = intermediate_label)
2102 self.SetItemBold(intermediate_nodes[intermediate_label], bold = True)
2103 self.SetItemData(intermediate_nodes[intermediate_label], {'pk_health_issue': doc['pk_health_issue']})
2104 self.SetItemHasChildren(intermediate_nodes[intermediate_label], True)
2105 parent = intermediate_nodes[intermediate_label]
2106
2107 elif self.__sort_mode == 'org':
2108 if doc['pk_org'] is None:
2109 intermediate_label = _('unknown organization')
2110 else:
2111 if doc['unit_is_receiver']:
2112 direction = _('to: %s')
2113 else:
2114 direction = _('from: %s')
2115
2116 if doc['pk_org'] == gmPraxis.gmCurrentPraxisBranch()['pk_org']:
2117 org_str = _('this praxis')
2118 else:
2119 org_str = doc['organization']
2120 intermediate_label = direction % org_str
2121 doc_label = _('%s%7s %s:%s (%s)') % (
2122 gmTools.bool2subst(doc.has_unreviewed_parts, gmTools.u_writing_hand, '', '?'),
2123 gmDateTime.pydt_strftime(doc['clin_when'], '%m/%Y'),
2124 doc['l10n_type'][:26],
2125 gmTools.coalesce(value2test = doc['comment'], return_instead = '', template4value = ' %s'),
2126 no_parts
2127 )
2128 if intermediate_label not in intermediate_nodes:
2129 intermediate_nodes[intermediate_label] = self.AppendItem(parent = self.root, text = intermediate_label)
2130 self.SetItemBold(intermediate_nodes[intermediate_label], bold = True)
2131
2132 self.SetItemData(intermediate_nodes[intermediate_label], doc.org_unit)
2133 self.SetItemHasChildren(intermediate_nodes[intermediate_label], True)
2134 parent = intermediate_nodes[intermediate_label]
2135
2136 elif self.__sort_mode == 'age':
2137 intermediate_label = gmDateTime.pydt_strftime(doc['clin_when'], '%Y')
2138 doc_label = _('%s%7s %s:%s (%s)') % (
2139 gmTools.bool2subst(doc.has_unreviewed_parts, gmTools.u_writing_hand, '', '?'),
2140 gmDateTime.pydt_strftime(doc['clin_when'], '%b %d'),
2141 doc['l10n_type'][:26],
2142 gmTools.coalesce(value2test = doc['comment'], return_instead = '', template4value = ' %s'),
2143 no_parts
2144 )
2145 if intermediate_label not in intermediate_nodes:
2146 intermediate_nodes[intermediate_label] = self.AppendItem(parent = self.root, text = intermediate_label)
2147 self.SetItemBold(intermediate_nodes[intermediate_label], bold = True)
2148 self.SetItemData(intermediate_nodes[intermediate_label], doc['clin_when'])
2149 self.SetItemHasChildren(intermediate_nodes[intermediate_label], True)
2150 parent = intermediate_nodes[intermediate_label]
2151
2152 else:
2153 doc_label = _('%s%7s %s:%s (%s)') % (
2154 gmTools.bool2subst(doc.has_unreviewed_parts, gmTools.u_writing_hand, '', '?'),
2155 gmDateTime.pydt_strftime(doc['clin_when'], '%Y-%m'),
2156 doc['l10n_type'][:26],
2157 gmTools.coalesce(value2test = doc['comment'], return_instead = '', template4value = ' %s'),
2158 no_parts
2159 )
2160 parent = self.root
2161
2162 doc_node = self.AppendItem(parent = parent, text = doc_label)
2163
2164 self.SetItemData(doc_node, doc)
2165 if len(parts) == 0:
2166 self.SetItemHasChildren(doc_node, False)
2167 else:
2168 self.SetItemHasChildren(doc_node, True)
2169
2170
2171 for part in parts:
2172 f_ext = ''
2173 if part['filename'] is not None:
2174 f_ext = os.path.splitext(part['filename'])[1].strip('.').strip()
2175 if f_ext != '':
2176 f_ext = ' .' + f_ext.upper()
2177 label = '%s%s (%s%s)%s' % (
2178 gmTools.bool2str (
2179 boolean = part['reviewed'] or part['reviewed_by_you'] or part['reviewed_by_intended_reviewer'],
2180 true_str = '',
2181 false_str = gmTools.u_writing_hand
2182 ),
2183 _('part %2s') % part['seq_idx'],
2184 gmTools.size2str(part['size']),
2185 f_ext,
2186 gmTools.coalesce (
2187 part['obj_comment'],
2188 '',
2189 ': %s%%s%s' % (gmTools.u_left_double_angle_quote, gmTools.u_right_double_angle_quote)
2190 )
2191 )
2192
2193 part_node = self.AppendItem(parent = doc_node, text = label)
2194 self.SetItemData(part_node, part)
2195 self.SetItemHasChildren(part_node, False)
2196
2197 self.__sort_nodes()
2198 self.SelectItem(self.root)
2199
2200
2201 if self.__expanded_nodes is not None:
2202 self.ExpansionState = self.__expanded_nodes
2203
2204 self.Expand(self.root)
2205
2206
2207 if self.__expanded_nodes is None:
2208
2209 if self.__sort_mode in ['episode', 'type', 'issue', 'org']:
2210 for key in intermediate_nodes.keys():
2211 self.Expand(intermediate_nodes[key])
2212
2213 wx.EndBusyCursor()
2214
2215 return True
2216
2217
2219 """Used in sorting items.
2220
2221 -1: 1 < 2
2222 0: 1 = 2
2223 1: 1 > 2
2224 """
2225
2226 if not node1:
2227 _log.debug('invalid node 1')
2228 return 0
2229 if not node2:
2230 _log.debug('invalid node 2')
2231 return 0
2232 if not node1.IsOk():
2233 _log.debug('no data on node 1')
2234 return 0
2235 if not node2.IsOk():
2236 _log.debug('no data on node 2')
2237 return 0
2238
2239 data1 = self.GetItemData(node1)
2240 data2 = self.GetItemData(node2)
2241
2242
2243 if isinstance(data1, gmDocuments.cDocument):
2244 date_field = 'clin_when'
2245
2246 if self.__sort_mode == 'age':
2247
2248 if data1[date_field] > data2[date_field]:
2249 return -1
2250 if data1[date_field] == data2[date_field]:
2251 return 0
2252 return 1
2253 if self.__sort_mode == 'episode':
2254 if data1['episode'] < data2['episode']:
2255 return -1
2256 if data1['episode'] == data2['episode']:
2257
2258 if data1[date_field] > data2[date_field]:
2259 return -1
2260 if data1[date_field] == data2[date_field]:
2261 return 0
2262 return 1
2263 return 1
2264 if self.__sort_mode == 'issue':
2265 if data1['health_issue'] == data2['health_issue']:
2266
2267 if data1[date_field] > data2[date_field]:
2268 return -1
2269 if data1[date_field] == data2[date_field]:
2270 return 0
2271 return 1
2272 if data1['health_issue'] < data2['health_issue']:
2273 return -1
2274 return 1
2275 if self.__sort_mode == 'review':
2276
2277 if data1.has_unreviewed_parts == data2.has_unreviewed_parts:
2278
2279 if data1[date_field] > data2[date_field]:
2280 return -1
2281 if data1[date_field] == data2[date_field]:
2282 return 0
2283 return 1
2284 if data1.has_unreviewed_parts:
2285 return -1
2286 return 1
2287 if self.__sort_mode == 'type':
2288 if data1['l10n_type'] < data2['l10n_type']:
2289 return -1
2290 if data1['l10n_type'] == data2['l10n_type']:
2291
2292 if data1[date_field] > data2[date_field]:
2293 return -1
2294 if data1[date_field] == data2[date_field]:
2295 return 0
2296 return 1
2297 return 1
2298 if self.__sort_mode == 'org':
2299 if (data1['organization'] is None) and (data2['organization'] is None):
2300 return 0
2301 if (data1['organization'] is None) and (data2['organization'] is not None):
2302 return 1
2303 if (data1['organization'] is not None) and (data2['organization'] is None):
2304 return -1
2305 txt1 = '%s %s' % (data1['organization'], data1['unit'])
2306 txt2 = '%s %s' % (data2['organization'], data2['unit'])
2307 if txt1 < txt2:
2308 return -1
2309 if txt1 == txt2:
2310
2311 if data1[date_field] > data2[date_field]:
2312 return -1
2313 if data1[date_field] == data2[date_field]:
2314 return 0
2315 return 1
2316 return 1
2317
2318 _log.error('unknown document sort mode [%s], reverse-sorting by age', self.__sort_mode)
2319
2320 if data1[date_field] > data2[date_field]:
2321 return -1
2322 if data1[date_field] == data2[date_field]:
2323 return 0
2324 return 1
2325
2326
2327 if isinstance(data1, gmDocuments.cDocumentPart):
2328
2329
2330 if data1['seq_idx'] < data2['seq_idx']:
2331 return -1
2332 if data1['seq_idx'] == data2['seq_idx']:
2333 return 0
2334 return 1
2335
2336
2337 if isinstance(data1, gmOrganization.cOrgUnit):
2338 l1 = self.GetItemText(node1)
2339 l2 = self.GetItemText(node2)
2340 if l1 < l2:
2341 return -1
2342 if l1 == l2:
2343 return 0
2344 return 1
2345
2346
2347 if isinstance(data1, dict):
2348 if ('pk_episode' in data1) or ('pk_health_issue' in data1):
2349 l1 = self.GetItemText(node1)
2350 l2 = self.GetItemText(node2)
2351 if l1 < l2:
2352 return -1
2353 if l1 == l2:
2354 return 0
2355 return 1
2356 _log.error('dict but unknown content: %s', data1.keys())
2357 return 1
2358
2359
2360 if isinstance(data1, pydt.datetime):
2361 y1 = gmDateTime.pydt_strftime(data1, '%Y')
2362 y2 = gmDateTime.pydt_strftime(data2, '%Y')
2363
2364 if y1 < y2:
2365 return 1
2366 if y1 == y2:
2367 return 0
2368 return -1
2369
2370
2371
2372 if None in [data1, data2]:
2373 l1 = self.GetItemText(node1)
2374 l2 = self.GetItemText(node2)
2375 if l1 < l2:
2376 return -1
2377 if l1 == l2:
2378 return 0
2379 else:
2380 if data1 < data2:
2381 return -1
2382 if data1 == data2:
2383 return 0
2384 return 1
2385
2386
2387
2388
2390 self.__expanded_nodes = self.ExpansionState
2391 self._schedule_data_reget()
2392
2393
2394 - def _on_doc_page_mod_db(self, *args, **kwargs):
2395 self.__expanded_nodes = self.ExpansionState
2396 self._schedule_data_reget()
2397
2398
2400
2401 if self.root is not None:
2402 self.DeleteAllItems()
2403 self.root = None
2404
2405
2406 - def _on_post_patient_selection(self, *args, **kwargs):
2407
2408 self.__expanded_nodes = None
2409 self._schedule_data_reget()
2410
2411
2413 if self.__curr_node_data is None:
2414 return
2415
2416
2417 if self.__curr_node_data is None:
2418 self.__show_details_callback(document = None, part = None)
2419 return
2420
2421
2422 if isinstance(self.__curr_node_data, gmDocuments.cDocument):
2423 self.__show_details_callback(document = self.__curr_node_data, part = None)
2424 return
2425
2426 if isinstance(self.__curr_node_data, gmDocuments.cDocumentPart):
2427 doc = self.GetItemData(self.GetItemParent(self.__curr_node))
2428 self.__show_details_callback(document = doc, part = self.__curr_node_data)
2429 return
2430
2431 if isinstance(self.__curr_node_data, gmOrganization.cOrgUnit):
2432 self.__show_details_callback(org_unit = self.__curr_node_data)
2433 return
2434
2435 if isinstance(self.__curr_node_data, pydt.datetime):
2436
2437 return
2438
2439 if isinstance(self.__curr_node_data, dict):
2440 _log.debug('node data is dict: %s', self.__curr_node_data)
2441 try:
2442 issue = gmEMRStructItems.cHealthIssue(aPK_obj = self.__curr_node_data['pk_health_issue'])
2443 except KeyError:
2444 _log.debug('node data dict holds pseudo-issue for unattributed episodes, ignoring')
2445 issue = None
2446 try:
2447 episode = gmEMRStructItems.cEpisode(aPK_obj = self.__curr_node_data['pk_episode'])
2448 except KeyError:
2449 episode = None
2450 self.__show_details_callback(issue = issue, episode = episode)
2451 return
2452
2453
2454
2455
2456
2457
2458 raise ValueError('invalid document tree node data type: %s' % type(self.__curr_node_data))
2459
2460
2462 event.Skip()
2463 self.__curr_node = event.GetItem()
2464 self.__curr_node_data = self.GetItemData(self.__curr_node)
2465 self.__update_details_view()
2466
2467
2469 node = event.GetItem()
2470 node_data = self.GetItemData(node)
2471
2472
2473 if node_data is None:
2474 return None
2475
2476 if isinstance(node_data, gmDocuments.cDocumentPart):
2477 self.__display_part(part = node_data)
2478 return True
2479
2480 event.Skip()
2481
2482
2484
2485 self.__curr_node_data = self.GetItemData(evt.Item)
2486
2487
2488 if self.__curr_node_data is None:
2489 return None
2490
2491
2492 if isinstance(self.__curr_node_data, gmDocuments.cDocument):
2493 self.__handle_doc_context()
2494
2495
2496 if isinstance(self.__curr_node_data, gmDocuments.cDocumentPart):
2497 self.__handle_part_context()
2498
2499 del self.__curr_node_data
2500 evt.Skip()
2501
2502
2505
2506
2508 self.__display_part(part = self.__curr_node_data)
2509
2510
2512 self.__review_part(part = self.__curr_node_data)
2513
2514
2517
2518
2546
2547
2548
2549
2551
2552 if start_node is None:
2553 start_node = self.GetRootItem()
2554
2555
2556
2557 if not start_node.IsOk():
2558 return True
2559
2560 self.SortChildren(start_node)
2561
2562 child_node, cookie = self.GetFirstChild(start_node)
2563 while child_node.IsOk():
2564 self.__sort_nodes(start_node = child_node)
2565 child_node, cookie = self.GetNextChild(start_node, cookie)
2566
2567 return
2568
2569
2571 self.PopupMenu(self.__doc_context_menu, wx.DefaultPosition)
2572
2573
2575 ID = None
2576
2577 if self.__curr_node_data['type'] == 'patient photograph':
2578 item = self.__part_context_menu.Append(-1, _('Activate as current photo'))
2579 self.Bind(wx.EVT_MENU, self.__activate_as_current_photo, item)
2580 ID = item.Id
2581
2582 self.PopupMenu(self.__part_context_menu, wx.DefaultPosition)
2583
2584 if ID is not None:
2585 self.__part_context_menu.Delete(ID)
2586
2587
2588
2589
2591 """Display document part."""
2592
2593
2594 if part['size'] == 0:
2595 _log.debug('cannot display part [%s] - 0 bytes', part['pk_obj'])
2596 gmGuiHelpers.gm_show_error (
2597 aMessage = _('Document part does not seem to exist in database !'),
2598 aTitle = _('showing document')
2599 )
2600 return None
2601
2602 wx.BeginBusyCursor()
2603
2604 cfg = gmCfg.cCfgSQL()
2605
2606
2607 chunksize = int(
2608 cfg.get2 (
2609 option = "horstspace.blob_export_chunk_size",
2610 workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace,
2611 bias = 'workplace',
2612 default = default_chunksize
2613 ))
2614
2615
2616 block_during_view = bool( cfg.get2 (
2617 option = 'horstspace.document_viewer.block_during_view',
2618 workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace,
2619 bias = 'user',
2620 default = None
2621 ))
2622
2623
2624 successful, msg = part.display_via_mime (
2625 chunksize = chunksize,
2626 block = block_during_view
2627 )
2628
2629 wx.EndBusyCursor()
2630
2631 if not successful:
2632 gmGuiHelpers.gm_show_error (
2633 aMessage = _('Cannot display document part:\n%s') % msg,
2634 aTitle = _('showing document')
2635 )
2636 return None
2637
2638
2639
2640
2641
2642
2643
2644 review_after_display = int(cfg.get2 (
2645 option = 'horstspace.document_viewer.review_after_display',
2646 workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace,
2647 bias = 'user',
2648 default = 3
2649 ))
2650 if review_after_display == 1:
2651 self.__review_part(part=part)
2652 elif review_after_display == 2:
2653 review_by_me = [ rev for rev in part.get_reviews() if rev['is_your_review'] ]
2654 if len(review_by_me) == 0:
2655 self.__review_part(part = part)
2656 elif review_after_display == 3:
2657 if len(part.get_reviews()) == 0:
2658 self.__review_part(part = part)
2659 elif review_after_display == 4:
2660 reviewed_by_responsible = [ rev for rev in part.get_reviews() if rev['is_review_by_responsible_reviewer'] ]
2661 if len(reviewed_by_responsible) == 0:
2662 self.__review_part(part = part)
2663
2664 return True
2665
2667 dlg = cReviewDocPartDlg (
2668 parent = self,
2669 id = -1,
2670 part = part
2671 )
2672 dlg.ShowModal()
2673 dlg.DestroyLater()
2674
2676 target_doc = manage_documents (
2677 parent = self,
2678 msg = _('\nSelect the document into which to move the selected part !\n')
2679 )
2680 if target_doc is None:
2681 return
2682 if not self.__curr_node_data.reattach(pk_doc = target_doc['pk_doc']):
2683 gmGuiHelpers.gm_show_error (
2684 aMessage = _('Cannot move document part.'),
2685 aTitle = _('Moving document part')
2686 )
2687
2689 delete_it = gmGuiHelpers.gm_show_question (
2690 cancel_button = True,
2691 title = _('Deleting document part'),
2692 question = _(
2693 'Are you sure you want to delete the %s part #%s\n'
2694 '\n'
2695 '%s'
2696 'from the following document\n'
2697 '\n'
2698 ' %s (%s)\n'
2699 '%s'
2700 '\n'
2701 'Really delete ?\n'
2702 '\n'
2703 '(this action cannot be reversed)'
2704 ) % (
2705 gmTools.size2str(self.__curr_node_data['size']),
2706 self.__curr_node_data['seq_idx'],
2707 gmTools.coalesce(self.__curr_node_data['obj_comment'], '', ' "%s"\n\n'),
2708 self.__curr_node_data['l10n_type'],
2709 gmDateTime.pydt_strftime(self.__curr_node_data['date_generated'], format = '%Y-%m-%d', accuracy = gmDateTime.acc_days),
2710 gmTools.coalesce(self.__curr_node_data['doc_comment'], '', ' "%s"\n')
2711 )
2712 )
2713 if not delete_it:
2714 return
2715
2716 gmDocuments.delete_document_part (
2717 part_pk = self.__curr_node_data['pk_obj'],
2718 encounter_pk = gmPerson.gmCurrentPatient().emr.active_encounter['pk_encounter']
2719 )
2720
2722
2723 gmHooks.run_hook_script(hook = 'before_%s_doc_part' % action)
2724
2725 wx.BeginBusyCursor()
2726
2727
2728 found, external_cmd = gmShellAPI.detect_external_binary('gm-%s_doc' % action)
2729 if not found:
2730 found, external_cmd = gmShellAPI.detect_external_binary('gm-%s_doc.bat' % action)
2731 if not found:
2732 _log.error('neither of gm-%s_doc or gm-%s_doc.bat found', action, action)
2733 wx.EndBusyCursor()
2734 gmGuiHelpers.gm_show_error (
2735 _('Cannot %(l10n_action)s document part - %(l10n_action)s command not found.\n'
2736 '\n'
2737 'Either of gm-%(action)s_doc or gm-%(action)s_doc.bat\n'
2738 'must be in the execution path. The command will\n'
2739 'be passed the filename to %(l10n_action)s.'
2740 ) % {'action': action, 'l10n_action': l10n_action},
2741 _('Processing document part: %s') % l10n_action
2742 )
2743 return
2744
2745 cfg = gmCfg.cCfgSQL()
2746
2747
2748 chunksize = int(cfg.get2 (
2749 option = "horstspace.blob_export_chunk_size",
2750 workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace,
2751 bias = 'workplace',
2752 default = default_chunksize
2753 ))
2754
2755 part_file = self.__curr_node_data.save_to_file(aChunkSize = chunksize)
2756
2757 if action == 'print':
2758 cmd = '%s generic_document %s' % (external_cmd, part_file)
2759 else:
2760 cmd = '%s %s' % (external_cmd, part_file)
2761 if os.name == 'nt':
2762 blocking = True
2763 else:
2764 blocking = False
2765 success = gmShellAPI.run_command_in_shell (
2766 command = cmd,
2767 blocking = blocking
2768 )
2769
2770 wx.EndBusyCursor()
2771
2772 if not success:
2773 _log.error('%s command failed: [%s]', action, cmd)
2774 gmGuiHelpers.gm_show_error (
2775 _('Cannot %(l10n_action)s document part - %(l10n_action)s command failed.\n'
2776 '\n'
2777 'You may need to check and fix either of\n'
2778 ' gm-%(action)s_doc (Unix/Mac) or\n'
2779 ' gm-%(action)s_doc.bat (Windows)\n'
2780 '\n'
2781 'The command is passed the filename to %(l10n_action)s.'
2782 ) % {'action': action, 'l10n_action': l10n_action},
2783 _('Processing document part: %s') % l10n_action
2784 )
2785 else:
2786 if action == 'mail':
2787 curr_pat = gmPerson.gmCurrentPatient()
2788 emr = curr_pat.emr
2789 emr.add_clin_narrative (
2790 soap_cat = None,
2791 note = _('document part handed over to email program: %s') % self.__curr_node_data.format(single_line = True),
2792 episode = self.__curr_node_data['pk_episode']
2793 )
2794
2796 self.__process_part(action = 'print', l10n_action = _('print'))
2797
2799 self.__process_part(action = 'fax', l10n_action = _('fax'))
2800
2802 self.__process_part(action = 'mail', l10n_action = _('mail'))
2803
2805 """Save document part into directory."""
2806 dlg = wx.DirDialog (
2807 parent = self,
2808 message = _('Save document part to directory ...'),
2809 defaultPath = os.path.expanduser(os.path.join('~', 'gnumed')),
2810 style = wx.DD_DEFAULT_STYLE
2811 )
2812 result = dlg.ShowModal()
2813 dirname = dlg.GetPath()
2814 dlg.DestroyLater()
2815
2816 if result != wx.ID_OK:
2817 return True
2818
2819 wx.BeginBusyCursor()
2820
2821 pat = gmPerson.gmCurrentPatient()
2822 fname = self.__curr_node_data.get_useful_filename (
2823 patient = pat,
2824 make_unique = True,
2825 directory = dirname
2826 )
2827
2828 cfg = gmCfg.cCfgSQL()
2829
2830
2831 chunksize = int(cfg.get2 (
2832 option = "horstspace.blob_export_chunk_size",
2833 workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace,
2834 bias = 'workplace',
2835 default = default_chunksize
2836 ))
2837
2838 fname = self.__curr_node_data.save_to_file (
2839 aChunkSize = chunksize,
2840 filename = fname,
2841 target_mime = None
2842 )
2843
2844 wx.EndBusyCursor()
2845
2846 gmDispatcher.send(signal = 'statustext', msg = _('Successfully saved document part as [%s].') % fname)
2847
2848 return True
2849
2850
2851
2852
2862
2866
2868
2869 gmHooks.run_hook_script(hook = 'before_%s_doc' % action)
2870
2871 wx.BeginBusyCursor()
2872
2873
2874 found, external_cmd = gmShellAPI.detect_external_binary('gm-%s_doc' % action)
2875 if not found:
2876 found, external_cmd = gmShellAPI.detect_external_binary('gm-%s_doc.bat' % action)
2877 if not found:
2878 _log.error('neither of gm-%s_doc or gm-%s_doc.bat found', action, action)
2879 wx.EndBusyCursor()
2880 gmGuiHelpers.gm_show_error (
2881 _('Cannot %(l10n_action)s document - %(l10n_action)s command not found.\n'
2882 '\n'
2883 'Either of gm-%(action)s_doc or gm-%(action)s_doc.bat\n'
2884 'must be in the execution path. The command will\n'
2885 'be passed a list of filenames to %(l10n_action)s.'
2886 ) % {'action': action, 'l10n_action': l10n_action},
2887 _('Processing document: %s') % l10n_action
2888 )
2889 return
2890
2891 cfg = gmCfg.cCfgSQL()
2892
2893
2894 chunksize = int(cfg.get2 (
2895 option = "horstspace.blob_export_chunk_size",
2896 workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace,
2897 bias = 'workplace',
2898 default = default_chunksize
2899 ))
2900
2901 part_files = self.__curr_node_data.save_parts_to_files(chunksize = chunksize)
2902
2903 if os.name == 'nt':
2904 blocking = True
2905 else:
2906 blocking = False
2907
2908 if action == 'print':
2909 cmd = '%s %s %s' % (
2910 external_cmd,
2911 'generic_document',
2912 ' '.join(part_files)
2913 )
2914 else:
2915 cmd = external_cmd + ' ' + ' '.join(part_files)
2916 success = gmShellAPI.run_command_in_shell (
2917 command = cmd,
2918 blocking = blocking
2919 )
2920
2921 wx.EndBusyCursor()
2922
2923 if not success:
2924 _log.error('%s command failed: [%s]', action, cmd)
2925 gmGuiHelpers.gm_show_error (
2926 _('Cannot %(l10n_action)s document - %(l10n_action)s command failed.\n'
2927 '\n'
2928 'You may need to check and fix either of\n'
2929 ' gm-%(action)s_doc (Unix/Mac) or\n'
2930 ' gm-%(action)s_doc.bat (Windows)\n'
2931 '\n'
2932 'The command is passed a list of filenames to %(l10n_action)s.'
2933 ) % {'action': action, 'l10n_action': l10n_action},
2934 _('Processing document: %s') % l10n_action
2935 )
2936
2937
2939 self.__process_doc(action = 'print', l10n_action = _('print'))
2940
2941
2943 self.__process_doc(action = 'fax', l10n_action = _('fax'))
2944
2945
2947 self.__process_doc(action = 'mail', l10n_action = _('mail'))
2948
2949
2951 dlg = wx.FileDialog (
2952 parent = self,
2953 message = _('Choose a file'),
2954 defaultDir = os.path.expanduser(os.path.join('~', 'gnumed')),
2955 defaultFile = '',
2956 wildcard = "%s (*)|*|PNGs (*.png)|*.png|PDFs (*.pdf)|*.pdf|TIFFs (*.tif)|*.tif|JPEGs (*.jpg)|*.jpg|%s (*.*)|*.*" % (_('all files'), _('all files (Win)')),
2957 style = wx.FD_OPEN | wx.FD_FILE_MUST_EXIST | wx.FD_MULTIPLE
2958 )
2959 result = dlg.ShowModal()
2960 if result != wx.ID_CANCEL:
2961 self.__curr_node_data.add_parts_from_files(files = dlg.GetPaths(), reviewer = gmStaff.gmCurrentProvider()['pk_staff'])
2962 dlg.DestroyLater()
2963
2964
2979
2981
2982 gmHooks.run_hook_script(hook = 'before_external_doc_access')
2983
2984 wx.BeginBusyCursor()
2985
2986
2987 found, external_cmd = gmShellAPI.detect_external_binary('gm_access_external_doc.sh')
2988 if not found:
2989 found, external_cmd = gmShellAPI.detect_external_binary('gm_access_external_doc.bat')
2990 if not found:
2991 _log.error('neither of gm_access_external_doc.sh or .bat found')
2992 wx.EndBusyCursor()
2993 gmGuiHelpers.gm_show_error (
2994 _('Cannot access external document - access command not found.\n'
2995 '\n'
2996 'Either of gm_access_external_doc.sh or *.bat must be\n'
2997 'in the execution path. The command will be passed the\n'
2998 'document type and the reference URL for processing.'
2999 ),
3000 _('Accessing external document')
3001 )
3002 return
3003
3004 cmd = '%s "%s" "%s"' % (external_cmd, self.__curr_node_data['type'], self.__curr_node_data['ext_ref'])
3005 if os.name == 'nt':
3006 blocking = True
3007 else:
3008 blocking = False
3009 success = gmShellAPI.run_command_in_shell (
3010 command = cmd,
3011 blocking = blocking
3012 )
3013
3014 wx.EndBusyCursor()
3015
3016 if not success:
3017 _log.error('External access command failed: [%s]', cmd)
3018 gmGuiHelpers.gm_show_error (
3019 _('Cannot access external document - access command failed.\n'
3020 '\n'
3021 'You may need to check and fix either of\n'
3022 ' gm_access_external_doc.sh (Unix/Mac) or\n'
3023 ' gm_access_external_doc.bat (Windows)\n'
3024 '\n'
3025 'The command is passed the document type and the\n'
3026 'external reference URL on the command line.'
3027 ),
3028 _('Accessing external document')
3029 )
3030
3032 """Save document into directory.
3033
3034 - one file per object
3035 - into subdirectory named after patient
3036 """
3037 pat = gmPerson.gmCurrentPatient()
3038 def_dir = os.path.expanduser(os.path.join('~', 'gnumed', pat.subdir_name))
3039 gmTools.mkdir(def_dir)
3040
3041 dlg = wx.DirDialog (
3042 parent = self,
3043 message = _('Save document into directory ...'),
3044 defaultPath = def_dir,
3045 style = wx.DD_DEFAULT_STYLE
3046 )
3047 result = dlg.ShowModal()
3048 dirname = dlg.GetPath()
3049 dlg.DestroyLater()
3050
3051 if result != wx.ID_OK:
3052 return True
3053
3054 wx.BeginBusyCursor()
3055
3056 cfg = gmCfg.cCfgSQL()
3057
3058
3059 chunksize = int(cfg.get2 (
3060 option = "horstspace.blob_export_chunk_size",
3061 workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace,
3062 bias = 'workplace',
3063 default = default_chunksize
3064 ))
3065
3066 fnames = self.__curr_node_data.save_parts_to_files(export_dir = dirname, chunksize = chunksize)
3067
3068 wx.EndBusyCursor()
3069
3070 gmDispatcher.send(signal='statustext', msg=_('Successfully saved %s parts into the directory [%s].') % (len(fnames), dirname))
3071
3072 return True
3073
3074
3077
3078
3089
3090
3091
3092
3093
3094 from Gnumed.wxGladeWidgets.wxgPACSPluginPnl import wxgPACSPluginPnl
3095
3096 -class cPACSPluginPnl(wxgPACSPluginPnl, gmRegetMixin.cRegetOnPaintMixin):
3097
3108
3109
3110
3111
3113
3114 pool = gmConnectionPool.gmConnectionPool()
3115 self._TCTRL_host.Value = gmTools.coalesce(pool.credentials.host, 'localhost')
3116 self._TCTRL_port.Value = '8042'
3117
3118 self._LCTRL_studies.set_columns(columns = [_('Date'), _('Description'), _('Organization'), _('Authority')])
3119 self._LCTRL_studies.select_callback = self._on_studies_list_item_selected
3120 self._LCTRL_studies.deselect_callback = self._on_studies_list_item_deselected
3121
3122 self._LCTRL_series.set_columns(columns = [_('Time'), _('Method'), _('Body part'), _('Description')])
3123 self._LCTRL_series.select_callback = self._on_series_list_item_selected
3124 self._LCTRL_series.deselect_callback = self._on_series_list_item_deselected
3125
3126 self._LCTRL_details.set_columns(columns = [_('DICOM field'), _('Value')])
3127 self._LCTRL_details.set_column_widths()
3128
3129 self._BMP_preview.SetBitmap(wx.Bitmap.FromRGBA(50,50, red=0, green=0, blue=0, alpha = wx.ALPHA_TRANSPARENT))
3130
3131
3132 self.__thumbnail_menu = wx.Menu()
3133 item = self.__thumbnail_menu.Append(-1, _('Show in DICOM viewer'))
3134 self.Bind(wx.EVT_MENU, self._on_show_image_as_dcm, item)
3135 item = self.__thumbnail_menu.Append(-1, _('Show in image viewer'))
3136 self.Bind(wx.EVT_MENU, self._on_show_image_as_png, item)
3137 self.__thumbnail_menu.AppendSeparator()
3138 item = self.__thumbnail_menu.Append(-1, _('Copy to export area'))
3139 self.Bind(wx.EVT_MENU, self._on_copy_image_to_export_area, item)
3140 item = self.__thumbnail_menu.Append(-1, _('Save as DICOM file (.dcm)'))
3141 self.Bind(wx.EVT_MENU, self._on_save_image_as_dcm, item)
3142 item = self.__thumbnail_menu.Append(-1, _('Save as image file (.png)'))
3143 self.Bind(wx.EVT_MENU, self._on_save_image_as_png, item)
3144
3145
3146 self.__studies_menu = wx.Menu('Studies:')
3147 self.__studies_menu.AppendSeparator()
3148 item = self.__studies_menu.Append(-1, _('Show in DICOM viewer'))
3149 self.Bind(wx.EVT_MENU, self._on_studies_show_button_pressed, item)
3150 self.__studies_menu.AppendSeparator()
3151
3152 item = self.__studies_menu.Append(-1, _('Selected into export area'))
3153 self.Bind(wx.EVT_MENU, self._on_copy_selected_studies_to_export_area, item)
3154 item = self.__studies_menu.Append(-1, _('ZIP of selected into export area'))
3155 self.Bind(wx.EVT_MENU, self._on_copy_zip_of_selected_studies_to_export_area, item)
3156 item = self.__studies_menu.Append(-1, _('All into export area'))
3157 self.Bind(wx.EVT_MENU, self._on_copy_all_studies_to_export_area, item)
3158 item = self.__studies_menu.Append(-1, _('ZIP of all into export area'))
3159 self.Bind(wx.EVT_MENU, self._on_copy_zip_of_all_studies_to_export_area, item)
3160 self.__studies_menu.AppendSeparator()
3161
3162 item = self.__studies_menu.Append(-1, _('Save selected'))
3163 self.Bind(wx.EVT_MENU, self._on_save_selected_studies, item)
3164 item = self.__studies_menu.Append(-1, _('Save ZIP of selected'))
3165 self.Bind(wx.EVT_MENU, self._on_save_zip_of_selected_studies, item)
3166 item = self.__studies_menu.Append(-1, _('Save all'))
3167 self.Bind(wx.EVT_MENU, self._on_save_all_studies, item)
3168 item = self.__studies_menu.Append(-1, _('Save ZIP of all'))
3169 self.Bind(wx.EVT_MENU, self._on_save_zip_of_all_studies, item)
3170 self.__studies_menu.AppendSeparator()
3171
3172 item = self.__studies_menu.Append(-1, _('Add file to study (PDF/image)'))
3173 self.Bind(wx.EVT_MENU, self._on_add_file_to_study, item)
3174
3175
3234
3235
3237 self._LBL_patient_identification.SetLabel('')
3238 self._LCTRL_studies.set_string_items(items = [])
3239 self._LCTRL_series.set_string_items(items = [])
3240 self.__refresh_image()
3241 self.__refresh_details()
3242
3243
3245 self._LBL_PACS_identification.SetLabel(_('<not connected>'))
3246
3247
3249 self.__reset_server_identification()
3250 self.__reset_patient_data()
3251 self.__set_button_states()
3252
3253
3255
3256 self.__pacs = None
3257 self.__orthanc_patient = None
3258 self.__set_button_states()
3259 self.__reset_server_identification()
3260
3261 host = self._TCTRL_host.Value.strip()
3262 port = self._TCTRL_port.Value.strip()[:6]
3263 if port == '':
3264 self._LBL_PACS_identification.SetLabel(_('Cannot connect without port (try 8042).'))
3265 return False
3266 if len(port) < 4:
3267 return False
3268 try:
3269 int(port)
3270 except ValueError:
3271 self._LBL_PACS_identification.SetLabel(_('Invalid port (try 8042).'))
3272 return False
3273
3274 user = self._TCTRL_user.Value
3275 if user == '':
3276 user = None
3277 self._LBL_PACS_identification.SetLabel(_('Connect to [%s] @ port %s as "%s".') % (host, port, user))
3278 password = self._TCTRL_password.Value
3279 if password == '':
3280 password = None
3281
3282 pacs = gmDICOM.cOrthancServer()
3283 if not pacs.connect(host = host, port = port, user = user, password = password):
3284 self._LBL_PACS_identification.SetLabel(_('Cannot connect to PACS.'))
3285 _log.error('error connecting to server: %s', pacs.connect_error)
3286 return False
3287
3288
3289 self._LBL_PACS_identification.SetLabel(_('PACS: Orthanc "%s" (AET "%s", Version %s, DB v%s)') % (
3290 pacs.server_identification['Name'],
3291 pacs.server_identification['DicomAet'],
3292 pacs.server_identification['Version'],
3293
3294 pacs.server_identification['DatabaseVersion']
3295 ))
3296
3297 self.__pacs = pacs
3298 self.__set_button_states()
3299 return True
3300
3301
3303
3304 self.__orthanc_patient = None
3305
3306 if not self.__patient.connected:
3307 self.__reset_patient_data()
3308 self.__set_button_states()
3309 return True
3310
3311 if not self.__connect():
3312 return False
3313
3314 tt_lines = [_('Known PACS IDs:')]
3315 for pacs_id in self.__patient.suggest_external_ids(target = 'PACS'):
3316 tt_lines.append(' ' + _('generic: %s') % pacs_id)
3317 for pacs_id in self.__patient.get_external_ids(id_type = 'PACS', issuer = self.__pacs.as_external_id_issuer):
3318 tt_lines.append(' ' + _('stored: "%(value)s" @ [%(issuer)s]') % pacs_id)
3319 tt_lines.append('')
3320 tt_lines.append(_('Patients found in PACS:'))
3321
3322 info_lines = []
3323
3324 matching_pats = self.__pacs.get_matching_patients(person = self.__patient)
3325 if len(matching_pats) == 0:
3326 info_lines.append(_('PACS: no patients with matching IDs found'))
3327 no_of_studies = 0
3328 for pat in matching_pats:
3329 info_lines.append('"%s" %s "%s (%s) %s"' % (
3330 pat['MainDicomTags']['PatientID'],
3331 gmTools.u_arrow2right,
3332 gmTools.coalesce(pat['MainDicomTags']['PatientName'], '?'),
3333 gmTools.coalesce(pat['MainDicomTags']['PatientSex'], '?'),
3334 gmTools.coalesce(pat['MainDicomTags']['PatientBirthDate'], '?')
3335 ))
3336 no_of_studies += len(pat['Studies'])
3337 tt_lines.append('%s [#%s]' % (
3338 gmTools.format_dict_like (
3339 pat['MainDicomTags'],
3340 relevant_keys = ['PatientName', 'PatientSex', 'PatientBirthDate', 'PatientID'],
3341 template = ' %(PatientID)s = %(PatientName)s (%(PatientSex)s) %(PatientBirthDate)s',
3342 missing_key_template = '?'
3343 ),
3344 pat['ID']
3345 ))
3346 if len(matching_pats) > 1:
3347 info_lines.append(_('PACS: more than one patient with matching IDs found, carefully check studies'))
3348 self._LBL_patient_identification.SetLabel('\n'.join(info_lines))
3349 tt_lines.append('')
3350 tt_lines.append(_('Studies found: %s') % no_of_studies)
3351 self._LBL_patient_identification.SetToolTip('\n'.join(tt_lines))
3352
3353
3354 study_list_items = []
3355 study_list_data = []
3356 if len(matching_pats) > 0:
3357
3358 self.__orthanc_patient = matching_pats[0]
3359 for pat in self.__pacs.get_studies_list_by_orthanc_patient_list(orthanc_patients = matching_pats):
3360 for study in pat['studies']:
3361 docs = []
3362 if study['referring_doc'] is not None:
3363 docs.append(study['referring_doc'])
3364 if study['requesting_doc'] is not None:
3365 if study['requesting_doc'] not in docs:
3366 docs.append(study['requesting_doc'])
3367 if study['performing_doc'] is not None:
3368 if study['performing_doc'] not in docs:
3369 docs.append(study['requesting_doc'])
3370 if study['operator_name'] is not None:
3371 if study['operator_name'] not in docs:
3372 docs.append(study['operator_name'])
3373 if study['radiographer_code'] is not None:
3374 if study['radiographer_code'] not in docs:
3375 docs.append(study['radiographer_code'])
3376 org_name = u'@'.join ([
3377 o for o in [study['radiology_dept'], study['radiology_org']]
3378 if o is not None
3379 ])
3380 org = '%s%s%s' % (
3381 org_name,
3382 gmTools.coalesce(study['station_name'], '', ' [%s]'),
3383 gmTools.coalesce(study['radiology_org_addr'], '', ' (%s)').replace('\r\n', ' [CR] ')
3384 )
3385 if study['date'] is None:
3386 study_date = '?'
3387 else:
3388 study_date = '%s-%s-%s' % (
3389 study['date'][:4],
3390 study['date'][4:6],
3391 study['date'][6:8]
3392 )
3393 study_list_items.append ( [
3394 study_date,
3395 _('%s series%s') % (
3396 len(study['series']),
3397 gmTools.coalesce(study['description'], '', ': %s')
3398 ),
3399 org.strip(),
3400 gmTools.u_arrow2right.join(docs)
3401 ] )
3402 study_list_data.append(study)
3403
3404 self._LCTRL_studies.set_string_items(items = study_list_items)
3405 self._LCTRL_studies.set_data(data = study_list_data)
3406 self._LCTRL_studies.SortListItems(0, 0)
3407 self._LCTRL_studies.set_column_widths()
3408
3409 self.__refresh_image()
3410 self.__refresh_details()
3411 self.__set_button_states()
3412
3413 return True
3414
3415
3417
3418 self._LCTRL_details.remove_items_safely()
3419 if self.__pacs is None:
3420 return
3421
3422
3423 study_data = self._LCTRL_studies.get_selected_item_data(only_one = True)
3424 if study_data is None:
3425 return
3426 items = []
3427 items = [ [key, study_data['all_tags'][key]] for key in study_data['all_tags'] if ('%s' % study_data['all_tags'][key]).strip() != '' ]
3428
3429
3430 series = self._LCTRL_series.get_selected_item_data(only_one = True)
3431 if series is None:
3432 self._LCTRL_details.set_string_items(items = items)
3433 self._LCTRL_details.set_column_widths()
3434 return
3435 items.append ([
3436 ' %s ' % (gmTools.u_box_horiz_single * 5),
3437 '%s %s %s' % (
3438 gmTools.u_box_horiz_single * 3,
3439 _('Series'),
3440 gmTools.u_box_horiz_single * 10
3441 )
3442 ])
3443 items.extend([ [key, series['all_tags'][key]] for key in series['all_tags'] if ('%s' % series['all_tags'][key]).strip() != '' ])
3444
3445
3446 if self.__image_data is None:
3447 self._LCTRL_details.set_string_items(items = items)
3448 self._LCTRL_details.set_column_widths()
3449 return
3450 items.append ([
3451 ' %s ' % (gmTools.u_box_horiz_single * 5),
3452 '%s %s %s' % (
3453 gmTools.u_box_horiz_single * 3,
3454 _('Image'),
3455 gmTools.u_box_horiz_single * 10
3456 )
3457 ])
3458 tags = self.__pacs.get_instance_dicom_tags(instance_id = self.__image_data['uuid'])
3459 if tags is False:
3460 items.extend(['image', '<tags not found in PACS>'])
3461 else:
3462 items.extend([ [key, tags[key]] for key in tags if ('%s' % tags[key]).strip() != '' ])
3463
3464 self._LCTRL_details.set_string_items(items = items)
3465 self._LCTRL_details.set_column_widths()
3466
3467
3469
3470 self.__image_data = None
3471 self._SZR_image_buttons.StaticBox.SetLabel(_('Image'))
3472 self._BMP_preview.SetBitmap(wx.Bitmap.FromRGBA(50,50, red=0, green=0, blue=0, alpha = wx.ALPHA_TRANSPARENT))
3473
3474 if idx is None:
3475 self._BMP_preview.ContainingSizer.Layout()
3476 return
3477 if self.__pacs is None:
3478 self._BMP_preview.ContainingSizer.Layout()
3479 return
3480 series = self._LCTRL_series.get_selected_item_data(only_one = True)
3481 if series is None:
3482 self._BMP_preview.ContainingSizer.Layout()
3483 return
3484 if idx > len(series['instances']) - 1:
3485 raise ValueError('trying to go beyond instances in series: %s of %s', idx, len(series['instances']))
3486
3487
3488 uuid = series['instances'][idx]
3489 img_file = self.__pacs.get_instance_preview(instance_id = uuid)
3490
3491 wx_bmp = gmGuiHelpers.file2scaled_image(filename = img_file, height = 100)
3492
3493 if wx_bmp is None:
3494 _log.error('cannot load DICOM instance from PACS: %s', uuid)
3495 else:
3496 self.__image_data = {'idx': idx, 'uuid': uuid}
3497 self._BMP_preview.SetBitmap(wx_bmp)
3498 self._SZR_image_buttons.StaticBox.SetLabel(_('Image %s/%s') % (idx+1, len(series['instances'])))
3499
3500 if idx == 0:
3501 self._BTN_previous_image.Disable()
3502 else:
3503 self._BTN_previous_image.Enable()
3504 if idx == len(series['instances']) - 1:
3505 self._BTN_next_image.Disable()
3506 else:
3507 self._BTN_next_image.Enable()
3508
3509 self._BMP_preview.ContainingSizer.Layout()
3510
3511
3513 if self.__image_data is None:
3514 return False
3515
3516 uuid = self.__image_data['uuid']
3517 img_file = None
3518 if as_dcm:
3519 img_file = self.__pacs.get_instance(instance_id = uuid)
3520 if as_png:
3521 img_file = self.__pacs.get_instance_preview(instance_id = uuid)
3522 if img_file is not None:
3523 (success, msg) = gmMimeLib.call_viewer_on_file(img_file)
3524 if not success:
3525 gmGuiHelpers.gm_show_warning (
3526 aMessage = _('Cannot show image:\n%s') % msg,
3527 aTitle = _('Previewing DICOM image')
3528 )
3529 return success
3530
3531
3532 img_file = self.__pacs.get_instance(instance_id = uuid)
3533 (success, msg) = gmMimeLib.call_viewer_on_file(img_file)
3534 if success:
3535 return True
3536
3537
3538 img_file = self.__pacs.get_instance_preview(instance_id = uuid)
3539 (success, msg) = gmMimeLib.call_viewer_on_file(img_file)
3540 if success:
3541 return True
3542
3543 gmGuiHelpers.gm_show_warning (
3544 aMessage = _('Cannot show in DICOM or image viewer:\n%s') % msg,
3545 aTitle = _('Previewing DICOM image')
3546 )
3547
3548
3549 - def __save_image(self, as_dcm=False, as_png=False, nice_filename=False):
3550 if self.__image_data is None:
3551 return False, None
3552
3553 fnames = {}
3554 uuid = self.__image_data['uuid']
3555 if as_dcm:
3556 if nice_filename:
3557 fname = gmTools.get_unique_filename (
3558 prefix = '%s-orthanc_%s--' % (self.__patient.subdir_name, uuid),
3559 suffix = '.dcm',
3560 tmp_dir = os.path.join(gmTools.gmPaths().home_dir, 'gnumed')
3561 )
3562 else:
3563 fname = None
3564 img_fname = self.__pacs.get_instance(filename = fname, instance_id = uuid)
3565 if img_fname is None:
3566 gmGuiHelpers.gm_show_warning (
3567 aMessage = _('Cannot save image as DICOM file.'),
3568 aTitle = _('Saving DICOM image')
3569 )
3570 return False, fnames
3571
3572 fnames['dcm'] = img_fname
3573 gmDispatcher.send(signal = 'statustext', msg = _('Successfully saved as [%s].') % img_fname)
3574
3575 if as_png:
3576 if nice_filename:
3577 fname = gmTools.get_unique_filename (
3578 prefix = '%s-orthanc_%s--' % (self.__patient.subdir_name, uuid),
3579 suffix = '.png',
3580 tmp_dir = os.path.join(gmTools.gmPaths().home_dir, 'gnumed')
3581 )
3582 else:
3583 fname = None
3584 img_fname = self.__pacs.get_instance_preview(filename = fname, instance_id = uuid)
3585 if img_fname is None:
3586 gmGuiHelpers.gm_show_warning (
3587 aMessage = _('Cannot save image as PNG file.'),
3588 aTitle = _('Saving DICOM image')
3589 )
3590 return False, fnames
3591 fnames['png'] = img_fname
3592 gmDispatcher.send(signal = 'statustext', msg = _('Successfully saved as [%s].') % img_fname)
3593
3594 return True, fnames
3595
3596
3598 if self.__image_data is None:
3599 return False
3600
3601 success, fnames = self.__save_image(as_dcm = True, as_png = True)
3602 if not success:
3603 return False
3604
3605 wx.BeginBusyCursor()
3606 self.__patient.export_area.add_files (
3607 filenames = [fnames['png'], fnames['dcm']],
3608 hint = _('DICOM image of [%s] from Orthanc PACS "%s" (AET "%s")') % (
3609 self.__orthanc_patient['MainDicomTags']['PatientID'],
3610 self.__pacs.server_identification['Name'],
3611 self.__pacs.server_identification['DicomAet']
3612 )
3613 )
3614 wx.EndBusyCursor()
3615
3616 gmDispatcher.send(signal = 'statustext', msg = _('Successfully stored in export area.'))
3617
3618
3619
3621 if self.__pacs is None:
3622 return
3623
3624 study_data = self._LCTRL_studies.get_selected_item_data(only_one = True)
3625 if len(study_data) == 0:
3626 return
3627
3628 gmNetworkTools.open_url_in_browser (
3629 self.__pacs.get_url_browse_study(study_id = study_data['orthanc_id']),
3630 new = 2,
3631 autoraise = True
3632 )
3633
3634
3636 if self.__pacs is None:
3637 return
3638
3639 study_data = self._LCTRL_studies.get_selected_item_data(only_one = False)
3640 if len(study_data) == 0:
3641 return
3642
3643 wx.BeginBusyCursor()
3644 target_dir = self.__pacs.get_studies_with_dicomdir(study_ids = [ s['orthanc_id'] for s in study_data ])
3645 wx.EndBusyCursor()
3646 if target_dir is False:
3647 gmGuiHelpers.gm_show_error (
3648 title = _('Showing DICOM studies'),
3649 error = _('Unable to show selected studies.')
3650 )
3651 return
3652 DICOMDIR = os.path.join(target_dir, 'DICOMDIR')
3653 if os.path.isfile(DICOMDIR):
3654 (success, msg) = gmMimeLib.call_viewer_on_file(DICOMDIR, block = False)
3655 if success:
3656 return
3657 else:
3658 _log.error('cannot find DICOMDIR in: %s', target_dir)
3659
3660 gmMimeLib.call_viewer_on_file(target_dir, block = False)
3661
3662
3663
3664
3666 if self.__pacs is None:
3667 return
3668
3669 study_data = self._LCTRL_studies.get_item_data()
3670 if len(study_data) == 0:
3671 return
3672
3673 self.__copy_studies_to_export_area(study_data)
3674
3675
3677 if self.__pacs is None:
3678 return
3679
3680 study_data = self._LCTRL_studies.get_selected_item_data(only_one = False)
3681 if len(study_data) == 0:
3682 return
3683
3684 self.__copy_studies_to_export_area(study_data)
3685
3686
3688 wx.BeginBusyCursor()
3689 target_dir = gmTools.mk_sandbox_dir (
3690 prefix = 'dcm-',
3691 base_dir = os.path.join(gmTools.gmPaths().home_dir, '.gnumed', self.__patient.subdir_name)
3692 )
3693 target_dir = self.__pacs.get_studies_with_dicomdir(study_ids = [ s['orthanc_id'] for s in study_data ], target_dir = target_dir)
3694 if target_dir is False:
3695 wx.EndBusyCursor()
3696 gmGuiHelpers.gm_show_error (
3697 title = _('Copying DICOM studies'),
3698 error = _('Unable to put studies into export area.')
3699 )
3700 return
3701
3702 comment = _('DICOM studies of [%s] from Orthanc PACS "%s" (AET "%s") [%s/]') % (
3703 self.__orthanc_patient['MainDicomTags']['PatientID'],
3704 self.__pacs.server_identification['Name'],
3705 self.__pacs.server_identification['DicomAet'],
3706 target_dir
3707 )
3708 if self.__patient.export_area.add_path(target_dir, comment):
3709 wx.EndBusyCursor()
3710 return
3711
3712 wx.EndBusyCursor()
3713 gmGuiHelpers.gm_show_error (
3714 title = _('Adding DICOM studies to export area'),
3715 error = _('Cannot add the following path to the export area:\n%s ') % target_dir
3716 )
3717
3718
3720 if self.__pacs is None:
3721 return
3722
3723 study_data = self._LCTRL_studies.get_item_data()
3724 if len(study_data) == 0:
3725 return
3726
3727 self.__copy_zip_of_studies_to_export_area(study_data)
3728
3729
3731 if self.__pacs is None:
3732 return
3733
3734 study_data = self._LCTRL_studies.get_selected_item_data(only_one = False)
3735 if len(study_data) == 0:
3736 return
3737
3738 self.__copy_zip_of_studies_to_export_area(study_data)
3739
3740
3742 wx.BeginBusyCursor()
3743 zip_fname = self.__pacs.get_studies_with_dicomdir (
3744 study_ids = [ s['orthanc_id'] for s in study_data ],
3745 create_zip = True
3746 )
3747 if zip_fname is False:
3748 wx.EndBusyCursor()
3749 gmGuiHelpers.gm_show_error (
3750 title = _('Adding DICOM studies to export area'),
3751 error = _('Unable to put ZIP of studies into export area.')
3752 )
3753 return
3754
3755
3756 zip_size = os.path.getsize(zip_fname)
3757 if zip_size > (300 * gmTools._MB):
3758 wx.EndBusyCursor()
3759 really_export = gmGuiHelpers.gm_show_question (
3760 title = _('Exporting DICOM studies'),
3761 question = _('The DICOM studies are %s in compressed size.\n\nReally move into export area ?') % gmTools.size2str(zip_size),
3762 cancel_button = False
3763 )
3764 if not really_export:
3765 return
3766
3767 hint = _('DICOM studies of [%s] from Orthanc PACS "%s" (AET "%s")') % (
3768 self.__orthanc_patient['MainDicomTags']['PatientID'],
3769 self.__pacs.server_identification['Name'],
3770 self.__pacs.server_identification['DicomAet']
3771 )
3772 if self.__patient.export_area.add_file(filename = zip_fname, hint = hint):
3773
3774 wx.EndBusyCursor()
3775 return
3776
3777 wx.EndBusyCursor()
3778 gmGuiHelpers.gm_show_error (
3779 title = _('Adding DICOM studies to export area'),
3780 error = _('Cannot add the following archive to the export area:\n%s ') % zip_fname
3781 )
3782
3783
3785 if self.__pacs is None:
3786 return
3787
3788 study_data = self._LCTRL_studies.get_selected_item_data(only_one = False)
3789 if len(study_data) == 0:
3790 return
3791
3792 self.__save_studies_to_disk(study_data)
3793
3794
3796 if self.__pacs is None:
3797 return
3798
3799 study_data = self._LCTRL_studies.get_item_data()
3800 if len(study_data) == 0:
3801 return
3802
3803 self.__save_studies_to_disk(study_data)
3804
3805
3807 default_path = os.path.join(gmTools.gmPaths().home_dir, 'gnumed', self.__patient.subdir_name)
3808 gmTools.mkdir(default_path)
3809 dlg = wx.DirDialog (
3810 self,
3811 message = _('Select the directory into which to save the DICOM studies.'),
3812 defaultPath = default_path
3813 )
3814 choice = dlg.ShowModal()
3815 target_dir = dlg.GetPath()
3816 dlg.DestroyLater()
3817 if choice != wx.ID_OK:
3818 return True
3819
3820 wx.BeginBusyCursor()
3821 target_dir = self.__pacs.get_studies_with_dicomdir(study_ids = [ s['orthanc_id'] for s in study_data ], target_dir = target_dir)
3822 wx.EndBusyCursor()
3823
3824 if target_dir is False:
3825 gmGuiHelpers.gm_show_error (
3826 title = _('Saving DICOM studies'),
3827 error = _('Unable to save DICOM studies.')
3828 )
3829 return
3830 gmDispatcher.send(signal = 'statustext', msg = _('Successfully saved to [%s].') % target_dir)
3831
3832
3834 if self.__pacs is None:
3835 return
3836
3837 study_data = self._LCTRL_studies.get_selected_item_data(only_one = False)
3838 if len(study_data) == 0:
3839 return
3840
3841 self.__save_zip_of_studies_to_disk(study_data)
3842
3843
3845 if self.__pacs is None:
3846 return
3847
3848 study_data = self._LCTRL_studies.get_item_data()
3849 if len(study_data) == 0:
3850 return
3851
3852 self.__save_zip_of_studies_to_disk(study_data)
3853
3854
3856 default_path = os.path.join(gmTools.gmPaths().home_dir, 'gnumed', self.__patient.subdir_name)
3857 gmTools.mkdir(default_path)
3858 dlg = wx.DirDialog (
3859 self,
3860 message = _('Select the directory into which to save the DICOM studies ZIP.'),
3861 defaultPath = default_path
3862 )
3863 choice = dlg.ShowModal()
3864 target_dir = dlg.GetPath()
3865 dlg.DestroyLater()
3866 if choice != wx.ID_OK:
3867 return True
3868
3869 wx.BeginBusyCursor()
3870 filename = self.__pacs.get_studies_with_dicomdir(study_ids = [ s['orthanc_id'] for s in study_data ], target_dir = target_dir, create_zip = True)
3871 wx.EndBusyCursor()
3872
3873 if filename is False:
3874 gmGuiHelpers.gm_show_error (
3875 title = _('Saving DICOM studies'),
3876 error = _('Unable to save DICOM studies as ZIP.')
3877 )
3878 return
3879
3880 gmDispatcher.send(signal = 'statustext', msg = _('Successfully saved as [%s].') % filename)
3881
3882
3884 if self.__pacs is None:
3885 return
3886
3887 study_data = self._LCTRL_studies.get_selected_item_data(only_one = False)
3888 if len(study_data) != 1:
3889 gmGuiHelpers.gm_show_info (
3890 title = _('Adding PDF to DICOM study'),
3891 info = _('For adding a PDF file there must be exactly one (1) DICOM study selected.')
3892 )
3893 return
3894
3895
3896 pdf_name = None
3897 dlg = wx.FileDialog (
3898 parent = self,
3899 message = _('Select PDF to add to DICOM study'),
3900 defaultDir = os.path.join(gmTools.gmPaths().home_dir, 'gnumed'),
3901 wildcard = "%s (*.pdf)|*.pdf|%s (*)|*" % (_('PDF files'), _('all files')),
3902 style = wx.FD_OPEN | wx.FD_FILE_MUST_EXIST
3903 )
3904 choice = dlg.ShowModal()
3905 pdf_name = dlg.GetPath()
3906 dlg.DestroyLater()
3907 if choice != wx.ID_OK:
3908 return
3909
3910 _log.debug('dicomize(%s)', pdf_name)
3911 if pdf_name is None:
3912 return
3913
3914
3915 instance_uuid = study_data[0]['series'][0]['instances'][-1]
3916 dcm_instance_template_fname = self.__pacs.get_instance(instance_id = instance_uuid)
3917
3918 _cfg = gmCfg2.gmCfgData()
3919 pdf2dcm_fname = gmDICOM.dicomize_pdf (
3920 pdf_name = pdf_name,
3921 dcm_template_file = dcm_instance_template_fname,
3922 title = 'GNUmed',
3923 verbose = _cfg.get(option = 'debug')
3924 )
3925 if pdf2dcm_fname is None:
3926 gmGuiHelpers.gm_show_error (
3927 title = _('Adding PDF to DICOM study'),
3928 error = _('Cannot turn PDF file\n\n %s\n\n into DICOM file.')
3929 )
3930 return
3931
3932
3933 if self.__pacs.upload_dicom_file(pdf2dcm_fname):
3934 gmDispatcher.send(signal = 'statustext', msg = _('Successfully uploaded [%s] to Orthanc DICOM server.') % pdf2dcm_fname)
3935 self._schedule_data_reget()
3936 return
3937
3938 gmGuiHelpers.gm_show_error (
3939 title = _('Adding PDF to DICOM study'),
3940 error = _('Cannot updload DICOM file\n\n %s\n\n into Orthanc PACS.') % pdf2dcm_fname
3941 )
3942
3943
3945 if self.__pacs is None:
3946 return
3947
3948 study_data = self._LCTRL_studies.get_selected_item_data(only_one = False)
3949 if len(study_data) != 1:
3950 gmGuiHelpers.gm_show_info (
3951 title = _('Adding file to DICOM study'),
3952 info = _('For adding a file there must be exactly one (1) DICOM study selected.')
3953 )
3954 return
3955
3956
3957 filename = None
3958 dlg = wx.FileDialog (
3959 parent = self,
3960 message = _('Select file (image or PDF) to add to DICOM study'),
3961 defaultDir = os.path.join(gmTools.gmPaths().home_dir, 'gnumed'),
3962 wildcard = "%s (*)|*|%s (*.pdf)|*.pdf" % (_('all files'), _('PDF files')),
3963 style = wx.FD_OPEN | wx.FD_FILE_MUST_EXIST
3964 )
3965 choice = dlg.ShowModal()
3966 filename = dlg.GetPath()
3967 dlg.DestroyLater()
3968 if choice != wx.ID_OK:
3969 return
3970
3971 if filename is None:
3972 return
3973
3974 _log.debug('dicomize(%s)', filename)
3975
3976 instance_uuid = study_data[0]['series'][0]['instances'][-1]
3977 dcm_instance_template_fname = self.__pacs.get_instance(instance_id = instance_uuid)
3978
3979 _cfg = gmCfg2.gmCfgData()
3980 dcm_fname = gmDICOM.dicomize_file (
3981 filename = filename,
3982 dcm_template_file = dcm_instance_template_fname,
3983 dcm_transfer_series = False,
3984 title = 'GNUmed',
3985 verbose = _cfg.get(option = 'debug')
3986 )
3987 if dcm_fname is None:
3988 gmGuiHelpers.gm_show_error (
3989 title = _('Adding file to DICOM study'),
3990 error = _('Cannot turn file\n\n %s\n\n into DICOM file.')
3991 )
3992 return
3993
3994
3995 if self.__pacs.upload_dicom_file(dcm_fname):
3996 gmDispatcher.send(signal = 'statustext', msg = _('Successfully uploaded [%s] to Orthanc DICOM server.') % dcm_fname)
3997 self._schedule_data_reget()
3998 return
3999
4000 gmGuiHelpers.gm_show_error (
4001 title = _('Adding file to DICOM study'),
4002 error = _('Cannot updload DICOM file\n\n %s\n\n into Orthanc PACS.') % dcm_fname
4003 )
4004
4005
4006
4008 if self.__pacs is None:
4009 return
4010
4011 gmNetworkTools.open_url_in_browser (
4012 self.__pacs.get_url_browse_patient(patient_id = self.__orthanc_patient['ID']),
4013 new = 2,
4014 autoraise = True
4015 )
4016
4017
4018
4028
4029
4030
4031
4033 if not self.__patient.connected:
4034 self.__reset_ui_content()
4035 return True
4036
4037 if not self.__refresh_patient_data():
4038 return False
4039
4040 return True
4041
4042
4043
4044
4046
4047
4048 self._BMP_preview.Bind(wx.EVT_LEFT_DCLICK, self._on_preview_image_leftdoubleclicked)
4049 self._BMP_preview.Bind(wx.EVT_RIGHT_UP, self._on_preview_image_rightclicked)
4050 self._BTN_browse_study.Bind(wx.EVT_RIGHT_UP, self._on_studies_button_rightclicked)
4051
4052
4053 gmDispatcher.connect(signal = 'pre_patient_unselection', receiver = self._on_pre_patient_unselection)
4054 gmDispatcher.connect(signal = 'post_patient_selection', receiver = self._on_post_patient_selection)
4055
4056
4057 gmDispatcher.connect(signal = 'gm_table_mod', receiver = self._on_database_signal)
4058
4059
4061
4062
4063
4064 self.__reset_patient_data()
4065
4066
4068 self._schedule_data_reget()
4069
4070
4072
4073 if not self.__patient.connected:
4074
4075
4076 return True
4077
4078 if kwds['pk_identity'] != self.__patient.ID:
4079 return True
4080
4081 if kwds['table'] == 'dem.lnk_identity2ext_id':
4082 self._schedule_data_reget()
4083 return True
4084
4085 return True
4086
4087
4088
4089
4091
4092 event.Skip()
4093 if self.__pacs is None:
4094 return
4095
4096 study_data = self._LCTRL_studies.get_selected_item_data(only_one = True)
4097 if study_data is None:
4098 return
4099
4100 series = self._LCTRL_series.get_selected_item_data(only_one = True)
4101 if series is None:
4102 self.__set_button_states()
4103 return
4104
4105 if len(series['instances']) == 0:
4106 self.__refresh_image()
4107 self.__refresh_details()
4108 self.__set_button_states()
4109 return
4110
4111
4112 self.__refresh_image(0)
4113 self.__refresh_details()
4114 self.__set_button_states()
4115 self._BTN_previous_image.Disable()
4116
4117
4119 event.Skip()
4120
4121 self.__refresh_image()
4122 self.__refresh_details()
4123 self.__set_button_states()
4124
4125
4127 event.Skip()
4128 if self.__pacs is None:
4129 return
4130
4131 study_data = self._LCTRL_studies.get_item_data(item_idx = event.Index)
4132 series_list_items = []
4133 series_list_data = []
4134 for series in study_data['series']:
4135
4136 series_time = ''
4137 if series['time'] is None:
4138 series['time'] = study_data['time']
4139 if series['time'] is None:
4140 series_time = '?'
4141 else:
4142 series_time = '%s:%s:%s' % (
4143 series['time'][:2],
4144 series['time'][2:4],
4145 series['time'][4:6]
4146 )
4147
4148 series_desc_parts = []
4149 if series['description'] is not None:
4150 if series['protocol'] is None:
4151 series_desc_parts.append(series['description'].strip())
4152 else:
4153 if series['description'].strip() not in series['protocol'].strip():
4154 series_desc_parts.append(series['description'].strip())
4155 if series['protocol'] is not None:
4156 series_desc_parts.append('[%s]' % series['protocol'].strip())
4157 if series['performed_procedure_step_description'] is not None:
4158 series_desc_parts.append(series['performed_procedure_step_description'].strip())
4159 if series['acquisition_device_processing_description'] is not None:
4160 series_desc_parts.append(series['acquisition_device_processing_description'].strip())
4161 series_desc = ' / '.join(series_desc_parts)
4162 if len(series_desc) > 0:
4163 series_desc = ': ' + series_desc
4164 series_desc = _('%s image(s)%s') % (len(series['instances']), series_desc)
4165
4166 series_list_items.append ([
4167 series_time,
4168 gmTools.coalesce(series['modality'], ''),
4169 gmTools.coalesce(series['body_part'], ''),
4170 series_desc
4171 ])
4172 series_list_data.append(series)
4173
4174 self._LCTRL_series.set_string_items(items = series_list_items)
4175 self._LCTRL_series.set_data(data = series_list_data)
4176 self._LCTRL_series.SortListItems(0)
4177
4178 self.__refresh_image()
4179 self.__refresh_details()
4180 self.__set_button_states()
4181
4182
4184 event.Skip()
4185
4186 self._LCTRL_series.remove_items_safely()
4187 self.__refresh_image()
4188 self.__refresh_details()
4189 self.__set_button_states()
4190
4191
4192
4193
4208
4209
4254
4255
4257 event.Skip()
4258 if self.__pacs is None:
4259 return
4260
4261 title = _('Working on: Orthanc "%s" (AET "%s" @ %s:%s, Version %s)') % (
4262 self.__pacs.server_identification['Name'],
4263 self.__pacs.server_identification['DicomAet'],
4264 self._TCTRL_host.Value.strip(),
4265 self._TCTRL_port.Value.strip(),
4266 self.__pacs.server_identification['Version']
4267 )
4268 dlg = cModifyOrthancContentDlg(self, -1, server = self.__pacs, title = title)
4269 dlg.ShowModal()
4270 dlg.DestroyLater()
4271 self._schedule_data_reget()
4272
4273
4274
4275
4277 self.__show_image(as_dcm = True)
4278
4279
4281 self.__show_image(as_png = True)
4282
4283
4285 self.__copy_image_to_export_area()
4286
4287
4289 self.__save_image(as_png = True, nice_filename = True)
4290
4291
4293 self.__save_image(as_dcm = True, nice_filename = True)
4294
4295
4296
4299
4300
4302 if self.__image_data is None:
4303 return False
4304
4305 self.PopupMenu(self.__thumbnail_menu)
4306
4307
4314
4315
4321
4322
4325
4326
4329
4330
4331
4332
4335
4336
4339
4340
4343
4344
4347
4348
4350 self.__copy_selected_studies_to_export_area()
4351
4352
4354 self.__copy_all_studies_to_export_area()
4355
4356
4358 self.__copy_zip_of_selected_studies_to_export_area()
4359
4360
4362 self.__copy_zip_of_all_studies_to_export_area()
4363
4364
4366 self.__save_selected_studies()
4367
4368
4370 self.__save_zip_of_selected_studies()
4371
4372
4374 self.__save_all_studies()
4375
4376
4378 self.__save_zip_of_all_studies()
4379
4380
4381
4382
4385
4386
4434
4435
4438
4439
4440 from Gnumed.wxGladeWidgets.wxgModifyOrthancContentDlg import wxgModifyOrthancContentDlg
4441
4442 -class cModifyOrthancContentDlg(wxgModifyOrthancContentDlg):
4443 - def __init__(self, *args, **kwds):
4444 self.__srv = kwds['server']
4445 del kwds['server']
4446 title = kwds['title']
4447 del kwds['title']
4448 wxgModifyOrthancContentDlg.__init__(self, *args, **kwds)
4449 self.SetTitle(title)
4450 self._LCTRL_patients.set_columns( [_('Patient ID'), _('Name'), _('Birth date'), _('Gender'), _('Orthanc')] )
4451
4452
4454 self._LCTRL_patients.set_string_items()
4455 search_term = self._TCTRL_search_term.Value.strip()
4456 if search_term == '':
4457 return
4458 pats = self.__srv.get_patients_by_name(name_parts = search_term.split(), fuzzy = True)
4459 if len(pats) == 0:
4460 return
4461 list_items = []
4462 list_data = []
4463 for pat in pats:
4464 mt = pat['MainDicomTags']
4465 try:
4466 gender = mt['PatientSex']
4467 except KeyError:
4468 gender = ''
4469 try:
4470 dob = mt['PatientBirthDate']
4471 except KeyError:
4472 dob = ''
4473 list_items.append([mt['PatientID'], mt['PatientName'], dob, gender, pat['ID']])
4474 list_data.append(mt['PatientID'])
4475 self._LCTRL_patients.set_string_items(list_items)
4476 self._LCTRL_patients.set_column_widths()
4477 self._LCTRL_patients.set_data(list_data)
4478
4479
4481 event.Skip()
4482 self.__refresh_patient_list()
4483
4484
4491
4492
4494 event.Skip()
4495 new_id = self._TCTRL_new_patient_id.Value.strip()
4496 if new_id == '':
4497 return
4498 pats = self._LCTRL_patients.get_selected_item_data(only_one = False)
4499 if len(pats) == 0:
4500 return
4501 really_modify = gmGuiHelpers.gm_show_question (
4502 title = _('Modifying patient ID'),
4503 question = _(
4504 'Really modify %s patient(s) to have the new patient ID\n\n'
4505 ' [%s]\n\n'
4506 'stored in the Orthanc DICOM server ?'
4507 ) % (
4508 len(pats),
4509 new_id
4510 ),
4511 cancel_button = True
4512 )
4513 if not really_modify:
4514 return
4515 all_modified = True
4516 for pat in pats:
4517 success = self.__srv.modify_patient_id(old_patient_id = pat, new_patient_id = new_id)
4518 if not success:
4519 all_modified = False
4520 self.__refresh_patient_list()
4521
4522 if not all_modified:
4523 gmGuiHelpers.gm_show_warning (
4524 aTitle = _('Modifying patient ID'),
4525 aMessage = _(
4526 'I was unable to modify all DICOM patients.\n'
4527 '\n'
4528 'Please refer to the log file.'
4529 )
4530 )
4531 return all_modified
4532
4533
4534
4536 event.Skip()
4537 dlg = wx.DirDialog (
4538 self,
4539 message = _('Select the directory from which to recursively upload DICOM files.'),
4540 defaultPath = os.path.join(gmTools.gmPaths().home_dir, 'gnumed')
4541 )
4542 choice = dlg.ShowModal()
4543 dicom_dir = dlg.GetPath()
4544 dlg.DestroyLater()
4545 if choice != wx.ID_OK:
4546 return True
4547 wx.BeginBusyCursor()
4548 try:
4549 uploaded, not_uploaded = self.__pacs.upload_from_directory (
4550 directory = dicom_dir,
4551 recursive = True,
4552 check_mime_type = False,
4553 ignore_other_files = True
4554 )
4555 finally:
4556 wx.EndBusyCursor()
4557 if len(not_uploaded) == 0:
4558 q = _('Delete the uploaded DICOM files now ?')
4559 else:
4560 q = _('Some files have not been uploaded.\n\nDo you want to delete those DICOM files which have been sent to the PACS successfully ?')
4561 _log.error('not uploaded:')
4562 for f in not_uploaded:
4563 _log.error(f)
4564 delete_uploaded = gmGuiHelpers.gm_show_question (
4565 title = _('Uploading DICOM files'),
4566 question = q,
4567 cancel_button = False
4568 )
4569 if not delete_uploaded:
4570 return
4571 wx.BeginBusyCursor()
4572 for f in uploaded:
4573 gmTools.remove_file(f)
4574 wx.EndBusyCursor()
4575
4576
4577
4578
4579 if __name__ == '__main__':
4580
4581 if len(sys.argv) < 2:
4582 sys.exit()
4583
4584 if sys.argv[1] != 'test':
4585 sys.exit()
4586
4587 from Gnumed.business import gmPersonSearch
4588 from Gnumed.wxpython import gmPatSearchWidgets
4589
4590
4592 app = wx.PyWidgetTester(size = (180, 20))
4593
4594 prw = cDocumentPhraseWheel(app.frame, -1)
4595 prw.set_context('pat', 12)
4596 app.frame.Show(True)
4597 app.MainLoop()
4598
4599
4600 test_document_prw()
4601