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