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