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
17
18 import wx
19 import wx.lib.mixins.treemixin as treemixin
20
21
22 if __name__ == '__main__':
23 sys.path.insert(0, '../../')
24 from Gnumed.pycommon import gmI18N
25 if __name__ == '__main__':
26 gmI18N.activate_locale()
27 gmI18N.install_domain(domain = 'gnumed')
28 from Gnumed.pycommon import gmCfg
29 from Gnumed.pycommon import gmPG2
30 from Gnumed.pycommon import gmMimeLib
31 from Gnumed.pycommon import gmMatchProvider
32 from Gnumed.pycommon import gmDispatcher
33 from Gnumed.pycommon import gmDateTime
34 from Gnumed.pycommon import gmTools
35 from Gnumed.pycommon import gmShellAPI
36 from Gnumed.pycommon import gmHooks
37 from Gnumed.pycommon import gmNetworkTools
38 from Gnumed.pycommon import gmMimeLib
39
40 from Gnumed.business import gmPerson
41 from Gnumed.business import gmStaff
42 from Gnumed.business import gmDocuments
43 from Gnumed.business import gmEMRStructItems
44 from Gnumed.business import gmPraxis
45 from Gnumed.business import gmDICOM
46 from Gnumed.business import gmProviderInbox
47 from Gnumed.business import gmOrganization
48
49 from Gnumed.wxpython import gmGuiHelpers
50 from Gnumed.wxpython import gmRegetMixin
51 from Gnumed.wxpython import gmPhraseWheel
52 from Gnumed.wxpython import gmPlugin
53 from Gnumed.wxpython import gmEncounterWidgets
54 from Gnumed.wxpython import gmListWidgets
55 from Gnumed.wxpython import gmRegetMixin
56
57
58 _log = logging.getLogger('gm.ui')
59
60
61 default_chunksize = 1 * 1024 * 1024
62
63
65
66
67 def delete_item(item):
68 doit = gmGuiHelpers.gm_show_question (
69 _( 'Are you sure you want to delete this\n'
70 'description from the document ?\n'
71 ),
72 _('Deleting document description')
73 )
74 if not doit:
75 return True
76
77 document.delete_description(pk = item[0])
78 return True
79
80 def add_item():
81 dlg = gmGuiHelpers.cMultilineTextEntryDlg (
82 parent,
83 -1,
84 title = _('Adding document description'),
85 msg = _('Below you can add a document description.\n')
86 )
87 result = dlg.ShowModal()
88 if result == wx.ID_SAVE:
89 document.add_description(dlg.value)
90
91 dlg.Destroy()
92 return True
93
94 def edit_item(item):
95 dlg = gmGuiHelpers.cMultilineTextEntryDlg (
96 parent,
97 -1,
98 title = _('Editing document description'),
99 msg = _('Below you can edit the document description.\n'),
100 text = item[1]
101 )
102 result = dlg.ShowModal()
103 if result == wx.ID_SAVE:
104 document.update_description(pk = item[0], description = dlg.value)
105
106 dlg.Destroy()
107 return True
108
109 def refresh_list(lctrl):
110 descriptions = document.get_descriptions()
111
112 lctrl.set_string_items(items = [
113 '%s%s' % ( (' '.join(regex.split('\r\n+|\r+|\n+|\t+', desc[1])))[:30], gmTools.u_ellipsis )
114 for desc in descriptions
115 ])
116 lctrl.set_data(data = descriptions)
117
118
119 gmListWidgets.get_choices_from_list (
120 parent = parent,
121 msg = _('Select the description you are interested in.\n'),
122 caption = _('Managing document descriptions'),
123 columns = [_('Description')],
124 edit_callback = edit_item,
125 new_callback = add_item,
126 delete_callback = delete_item,
127 refresh_callback = refresh_list,
128 single_selection = True,
129 can_return_empty = True
130 )
131
132 return True
133
134
136 try:
137 del kwargs['signal']
138 del kwargs['sender']
139 except KeyError:
140 pass
141 wx.CallAfter(save_file_as_new_document, **kwargs)
142
144 try:
145 del kwargs['signal']
146 del kwargs['sender']
147 except KeyError:
148 pass
149 wx.CallAfter(save_files_as_new_document, **kwargs)
150
151 -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):
161
162
163 -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):
164
165 pat = gmPerson.gmCurrentPatient()
166 if not pat.connected:
167 return None
168
169 emr = pat.emr
170
171 if parent is None:
172 parent = wx.GetApp().GetTopWindow()
173
174 if episode is None:
175 all_epis = emr.get_episodes()
176
177 if len(all_epis) == 0:
178 episode = emr.add_episode(episode_name = _('Documents'), is_open = False)
179 else:
180 from Gnumed.wxpython.gmEMRStructWidgets import cEpisodeListSelectorDlg
181 dlg = cEpisodeListSelectorDlg(parent, -1, episodes = all_epis)
182 dlg.SetTitle(_('Select the episode under which to file the document ...'))
183 btn_pressed = dlg.ShowModal()
184 episode = dlg.get_selected_item_data(only_one = True)
185 dlg.Destroy()
186
187 if (btn_pressed == wx.ID_CANCEL) or (episode is None):
188 if unlock_patient:
189 pat.locked = False
190 return None
191
192 wx.BeginBusyCursor()
193
194 if pk_document_type is None:
195 pk_document_type = gmDocuments.create_document_type(document_type = document_type)['pk_doc_type']
196
197 docs_folder = pat.get_document_folder()
198 doc = docs_folder.add_document (
199 document_type = pk_document_type,
200 encounter = emr.active_encounter['pk_encounter'],
201 episode = episode['pk_episode']
202 )
203 if doc is None:
204 wx.EndBusyCursor()
205 gmGuiHelpers.gm_show_error (
206 aMessage = _('Cannot create new document.'),
207 aTitle = _('saving document')
208 )
209 return False
210
211 if reference is not None:
212 doc['ext_ref'] = reference
213 if pk_org_unit is not None:
214 doc['pk_org_unit'] = pk_org_unit
215 if date_generated is not None:
216 doc['clin_when'] = date_generated
217 if comment is not None:
218 if comment != '':
219 doc['comment'] = comment
220 doc.save()
221
222 success, msg, filename = doc.add_parts_from_files(files = filenames, reviewer = reviewer)
223 if not success:
224 wx.EndBusyCursor()
225 gmGuiHelpers.gm_show_error (
226 aMessage = msg,
227 aTitle = _('saving document')
228 )
229 return False
230
231 if review_as_normal:
232 doc.set_reviewed(technically_abnormal = False, clinically_relevant = False)
233
234 if unlock_patient:
235 pat.locked = False
236
237 gmDispatcher.send(signal = 'statustext', msg = _('Imported new document from %s.') % filenames, beep = True)
238
239
240 cfg = gmCfg.cCfgSQL()
241 show_id = bool (
242 cfg.get2 (
243 option = 'horstspace.scan_index.show_doc_id',
244 workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace,
245 bias = 'user'
246 )
247 )
248
249 wx.EndBusyCursor()
250
251 if not show_id:
252 gmDispatcher.send(signal = 'statustext', msg = _('Successfully saved new document.'))
253 else:
254 if reference is None:
255 msg = _('Successfully saved the new document.')
256 else:
257 msg = _('The reference ID for the new document is:\n'
258 '\n'
259 ' <%s>\n'
260 '\n'
261 'You probably want to write it down on the\n'
262 'original documents.\n'
263 '\n'
264 "If you don't care about the ID you can switch\n"
265 'off this message in the GNUmed configuration.\n'
266 ) % reference
267 gmGuiHelpers.gm_show_info (
268 aMessage = msg,
269 aTitle = _('Saving document')
270 )
271
272
273 tmp_dir = gmTools.gmPaths().tmp_dir
274 files2remove = [ f for f in filenames if not f.startswith(tmp_dir) ]
275 if len(files2remove) > 0:
276 do_delete = gmGuiHelpers.gm_show_question (
277 _( 'Successfully imported files as document.\n'
278 '\n'
279 'Do you want to delete imported files from the filesystem ?\n'
280 '\n'
281 ' %s'
282 ) % '\n '.join(files2remove),
283 _('Removing files')
284 )
285 if do_delete:
286 for fname in files2remove:
287 gmTools.remove_file(fname)
288
289 return doc
290
291
292 gmDispatcher.connect(signal = 'import_document_from_file', receiver = _save_file_as_new_document)
293 gmDispatcher.connect(signal = 'import_document_from_files', receiver = _save_files_as_new_document)
294
295
297
299
300 gmPhraseWheel.cPhraseWheel.__init__(self, *args, **kwargs)
301
302 ctxt = {'ctxt_pat': {
303 'where_part': '(pk_patient = %(pat)s) AND',
304 'placeholder': 'pat'
305 }}
306
307 mp = gmMatchProvider.cMatchProvider_SQL2 (
308 queries = ["""
309 SELECT DISTINCT ON (list_label)
310 pk_doc AS data,
311 l10n_type || ' (' || to_char(clin_when, 'YYYY Mon DD') || ')' || coalesce(': ' || unit || '@' || organization, '') || ' - ' || episode || coalesce(' (' || health_issue || ')', '') AS list_label,
312 l10n_type || ' (' || to_char(clin_when, 'YYYY Mon DD') || ')' || coalesce(': ' || organization, '') || ' - ' || coalesce(' (' || health_issue || ')', episode) AS field_label
313 FROM blobs.v_doc_med
314 WHERE
315 %(ctxt_pat)s
316 (
317 l10n_type %(fragment_condition)s
318 OR
319 unit %(fragment_condition)s
320 OR
321 organization %(fragment_condition)s
322 OR
323 episode %(fragment_condition)s
324 OR
325 health_issue %(fragment_condition)s
326 )
327 ORDER BY list_label
328 LIMIT 25"""],
329 context = ctxt
330 )
331 mp.setThresholds(1, 3, 5)
332 mp.unset_context('pat')
333
334 self.matcher = mp
335 self.picklist_delay = 50
336 self.selection_only = True
337
338 self.SetToolTip(_('Select a document.'))
339
340
345
346
351
352
409
410
411
412
420
421
422 from Gnumed.wxGladeWidgets import wxgEditDocumentTypesDlg
423
425 """A dialog showing a cEditDocumentTypesPnl."""
426
429
430
431 from Gnumed.wxGladeWidgets import wxgEditDocumentTypesPnl
432
434 """A panel grouping together fields to edit the list of document types."""
435
441
445
447 gmDispatcher.connect(signal = 'blobs.doc_type_mod_db', receiver = self._on_doc_type_mod_db)
448
451
453
454 self._LCTRL_doc_type.DeleteAllItems()
455
456 doc_types = gmDocuments.get_document_types()
457 pos = len(doc_types) + 1
458
459 for doc_type in doc_types:
460 row_num = self._LCTRL_doc_type.InsertItem(pos, label = doc_type['type'])
461 self._LCTRL_doc_type.SetItem(index = row_num, column = 1, label = doc_type['l10n_type'])
462 if doc_type['is_user_defined']:
463 self._LCTRL_doc_type.SetItem(index = row_num, column = 2, label = ' X ')
464 if doc_type['is_in_use']:
465 self._LCTRL_doc_type.SetItem(index = row_num, column = 3, label = ' X ')
466
467 if len(doc_types) > 0:
468 self._LCTRL_doc_type.set_data(data = doc_types)
469 self._LCTRL_doc_type.SetColumnWidth(0, wx.LIST_AUTOSIZE)
470 self._LCTRL_doc_type.SetColumnWidth(1, wx.LIST_AUTOSIZE)
471 self._LCTRL_doc_type.SetColumnWidth(2, wx.LIST_AUTOSIZE_USEHEADER)
472 self._LCTRL_doc_type.SetColumnWidth(3, wx.LIST_AUTOSIZE_USEHEADER)
473
474 self._TCTRL_type.SetValue('')
475 self._TCTRL_l10n_type.SetValue('')
476
477 self._BTN_set_translation.Enable(False)
478 self._BTN_delete.Enable(False)
479 self._BTN_add.Enable(False)
480 self._BTN_reassign.Enable(False)
481
482 self._LCTRL_doc_type.SetFocus()
483
484
485
487 doc_type = self._LCTRL_doc_type.get_selected_item_data()
488
489 self._TCTRL_type.SetValue(doc_type['type'])
490 self._TCTRL_l10n_type.SetValue(doc_type['l10n_type'])
491
492 self._BTN_set_translation.Enable(True)
493 self._BTN_delete.Enable(not bool(doc_type['is_in_use']))
494 self._BTN_add.Enable(False)
495 self._BTN_reassign.Enable(True)
496
497 return
498
500 self._BTN_set_translation.Enable(False)
501 self._BTN_delete.Enable(False)
502 self._BTN_reassign.Enable(False)
503
504 self._BTN_add.Enable(True)
505
506 return
507
514
531
541
573
574
576 """Let user select a document type."""
578
579 gmPhraseWheel.cPhraseWheel.__init__(self, *args, **kwargs)
580
581 mp = gmMatchProvider.cMatchProvider_SQL2 (
582 queries = [
583 """SELECT
584 data,
585 field_label,
586 list_label
587 FROM ((
588 SELECT
589 pk_doc_type AS data,
590 l10n_type AS field_label,
591 l10n_type AS list_label,
592 1 AS rank
593 FROM blobs.v_doc_type
594 WHERE
595 is_user_defined IS True
596 AND
597 l10n_type %(fragment_condition)s
598 ) UNION (
599 SELECT
600 pk_doc_type AS data,
601 l10n_type AS field_label,
602 l10n_type AS list_label,
603 2 AS rank
604 FROM blobs.v_doc_type
605 WHERE
606 is_user_defined IS False
607 AND
608 l10n_type %(fragment_condition)s
609 )) AS q1
610 ORDER BY q1.rank, q1.list_label"""]
611 )
612 mp.setThresholds(2, 4, 6)
613
614 self.matcher = mp
615 self.picklist_delay = 50
616
617 self.SetToolTip(_('Select the document type.'))
618
620
621 doc_type = self.GetValue().strip()
622 if doc_type == '':
623 gmDispatcher.send(signal = 'statustext', msg = _('Cannot create document type without name.'), beep = True)
624 _log.debug('cannot create document type without name')
625 return
626
627 pk = gmDocuments.create_document_type(doc_type)['pk_doc_type']
628 if pk is None:
629 self.data = {}
630 else:
631 self.SetText (
632 value = doc_type,
633 data = pk
634 )
635
636
637
638
649
650
653
654
655 from Gnumed.wxGladeWidgets import wxgReviewDocPartDlg
656
659 """Support parts and docs now.
660 """
661 part = kwds['part']
662 del kwds['part']
663 wxgReviewDocPartDlg.wxgReviewDocPartDlg.__init__(self, *args, **kwds)
664
665 if isinstance(part, gmDocuments.cDocumentPart):
666 self.__part = part
667 self.__doc = self.__part.get_containing_document()
668 self.__reviewing_doc = False
669 elif isinstance(part, gmDocuments.cDocument):
670 self.__doc = part
671 if len(self.__doc.parts) == 0:
672 self.__part = None
673 else:
674 self.__part = self.__doc.parts[0]
675 self.__reviewing_doc = True
676 else:
677 raise ValueError('<part> must be gmDocuments.cDocument or gmDocuments.cDocumentPart instance, got <%s>' % type(part))
678
679 self.__init_ui_data()
680
681
682
683
685
686
687 self._PhWheel_episode.SetText('%s ' % self.__doc['episode'], self.__doc['pk_episode'])
688 self._PhWheel_doc_type.SetText(value = self.__doc['l10n_type'], data = self.__doc['pk_type'])
689 self._PhWheel_doc_type.add_callback_on_set_focus(self._on_doc_type_gets_focus)
690 self._PhWheel_doc_type.add_callback_on_lose_focus(self._on_doc_type_loses_focus)
691
692 if self.__reviewing_doc:
693 self._PRW_doc_comment.SetText(gmTools.coalesce(self.__doc['comment'], ''))
694 self._PRW_doc_comment.set_context(context = 'pk_doc_type', val = self.__doc['pk_type'])
695 else:
696 self._PRW_doc_comment.SetText(gmTools.coalesce(self.__part['obj_comment'], ''))
697
698 if self.__doc['pk_org_unit'] is not None:
699 self._PRW_org.SetText(value = '%s @ %s' % (self.__doc['unit'], self.__doc['organization']), data = self.__doc['pk_org_unit'])
700
701 if self.__doc['unit_is_receiver']:
702 self._RBTN_org_is_receiver.Value = True
703 else:
704 self._RBTN_org_is_source.Value = True
705
706 if self.__reviewing_doc:
707 self._PRW_org.Enable()
708 else:
709 self._PRW_org.Disable()
710
711 if self.__doc['pk_hospital_stay'] is not None:
712 self._PRW_hospital_stay.SetText(data = self.__doc['pk_hospital_stay'])
713
714 fts = gmDateTime.cFuzzyTimestamp(timestamp = self.__doc['clin_when'])
715 self._PhWheel_doc_date.SetText(fts.strftime('%Y-%m-%d'), fts)
716 self._TCTRL_reference.SetValue(gmTools.coalesce(self.__doc['ext_ref'], ''))
717 if self.__reviewing_doc:
718 self._TCTRL_filename.Enable(False)
719 self._SPINCTRL_seq_idx.Enable(False)
720 else:
721 self._TCTRL_filename.SetValue(gmTools.coalesce(self.__part['filename'], ''))
722 self._SPINCTRL_seq_idx.SetValue(gmTools.coalesce(self.__part['seq_idx'], 0))
723
724 self._LCTRL_existing_reviews.InsertColumn(0, _('who'))
725 self._LCTRL_existing_reviews.InsertColumn(1, _('when'))
726 self._LCTRL_existing_reviews.InsertColumn(2, _('+/-'))
727 self._LCTRL_existing_reviews.InsertColumn(3, _('!'))
728 self._LCTRL_existing_reviews.InsertColumn(4, _('comment'))
729
730 self.__reload_existing_reviews()
731
732 if self._LCTRL_existing_reviews.GetItemCount() > 0:
733 self._LCTRL_existing_reviews.SetColumnWidth(0, wx.LIST_AUTOSIZE)
734 self._LCTRL_existing_reviews.SetColumnWidth(1, wx.LIST_AUTOSIZE)
735 self._LCTRL_existing_reviews.SetColumnWidth(2, wx.LIST_AUTOSIZE_USEHEADER)
736 self._LCTRL_existing_reviews.SetColumnWidth(3, wx.LIST_AUTOSIZE_USEHEADER)
737 self._LCTRL_existing_reviews.SetColumnWidth(4, wx.LIST_AUTOSIZE)
738
739 if self.__part is None:
740 self._ChBOX_review.SetValue(False)
741 self._ChBOX_review.Enable(False)
742 self._ChBOX_abnormal.Enable(False)
743 self._ChBOX_relevant.Enable(False)
744 self._ChBOX_sign_all_pages.Enable(False)
745 else:
746 me = gmStaff.gmCurrentProvider()
747 if self.__part['pk_intended_reviewer'] == me['pk_staff']:
748 msg = _('(you are the primary reviewer)')
749 else:
750 other = gmStaff.cStaff(aPK_obj = self.__part['pk_intended_reviewer'])
751 msg = _('(someone else is the intended reviewer: %s)') % other['short_alias']
752 self._TCTRL_responsible.SetValue(msg)
753
754 if self.__part['reviewed_by_you']:
755 revs = self.__part.get_reviews()
756 for rev in revs:
757 if rev['is_your_review']:
758 self._ChBOX_abnormal.SetValue(bool(rev[2]))
759 self._ChBOX_relevant.SetValue(bool(rev[3]))
760 break
761
762 self._ChBOX_sign_all_pages.SetValue(self.__reviewing_doc)
763
764 return True
765
766
768 self._LCTRL_existing_reviews.DeleteAllItems()
769 if self.__part is None:
770 return True
771 revs = self.__part.get_reviews()
772 if len(revs) == 0:
773 return True
774
775 review_by_responsible_doc = None
776 reviews_by_others = []
777 for rev in revs:
778 if rev['is_review_by_responsible_reviewer'] and not rev['is_your_review']:
779 review_by_responsible_doc = rev
780 if not (rev['is_review_by_responsible_reviewer'] or rev['is_your_review']):
781 reviews_by_others.append(rev)
782
783 if review_by_responsible_doc is not None:
784 row_num = self._LCTRL_existing_reviews.InsertItem(sys.maxsize, label=review_by_responsible_doc[0])
785 self._LCTRL_existing_reviews.SetItemTextColour(row_num, column=wx.BLUE)
786 self._LCTRL_existing_reviews.SetItem(index = row_num, column=0, label=review_by_responsible_doc[0])
787 self._LCTRL_existing_reviews.SetItem(index = row_num, column=1, label=review_by_responsible_doc[1].strftime('%x %H:%M'))
788 if review_by_responsible_doc['is_technically_abnormal']:
789 self._LCTRL_existing_reviews.SetItem(index = row_num, column=2, label='X')
790 if review_by_responsible_doc['clinically_relevant']:
791 self._LCTRL_existing_reviews.SetItem(index = row_num, column=3, label='X')
792 self._LCTRL_existing_reviews.SetItem(index = row_num, column=4, label=review_by_responsible_doc[6])
793 row_num += 1
794 for rev in reviews_by_others:
795 row_num = self._LCTRL_existing_reviews.InsertItem(sys.maxsize, label=rev[0])
796 self._LCTRL_existing_reviews.SetItem(index = row_num, column=0, label=rev[0])
797 self._LCTRL_existing_reviews.SetItem(index = row_num, column=1, label=rev[1].strftime('%x %H:%M'))
798 if rev['is_technically_abnormal']:
799 self._LCTRL_existing_reviews.SetItem(index = row_num, column=2, label='X')
800 if rev['clinically_relevant']:
801 self._LCTRL_existing_reviews.SetItem(index = row_num, column=3, label='X')
802 self._LCTRL_existing_reviews.SetItem(index = row_num, column=4, label=rev[6])
803 return True
804
805
806
807
904
905
907 state = self._ChBOX_review.GetValue()
908 self._ChBOX_abnormal.Enable(enable = state)
909 self._ChBOX_relevant.Enable(enable = state)
910 self._ChBOX_responsible.Enable(enable = state)
911
912
914 """Per Jim: Changing the doc type happens a lot more often
915 then correcting spelling, hence select-all on getting focus.
916 """
917 self._PhWheel_doc_type.SetSelection(-1, -1)
918
919
921 pk_doc_type = self._PhWheel_doc_type.GetData()
922 if pk_doc_type is None:
923 self._PRW_doc_comment.unset_context(context = 'pk_doc_type')
924 else:
925 self._PRW_doc_comment.set_context(context = 'pk_doc_type', val = pk_doc_type)
926 return True
927
928
930
931 _log.debug('acquiring images from [%s]', device)
932
933
934
935 from Gnumed.pycommon import gmScanBackend
936 try:
937 fnames = gmScanBackend.acquire_pages_into_files (
938 device = device,
939 delay = 5,
940 calling_window = calling_window
941 )
942 except OSError:
943 _log.exception('problem acquiring image from source')
944 gmGuiHelpers.gm_show_error (
945 aMessage = _(
946 'No images could be acquired from the source.\n\n'
947 'This may mean the scanner driver is not properly installed.\n\n'
948 'On Windows you must install the TWAIN Python module\n'
949 'while on Linux and MacOSX it is recommended to install\n'
950 'the XSane package.'
951 ),
952 aTitle = _('Acquiring images')
953 )
954 return None
955
956 _log.debug('acquired %s images', len(fnames))
957
958 return fnames
959
960
961 from Gnumed.wxGladeWidgets import wxgScanIdxPnl
962
963 -class cScanIdxDocsPnl(wxgScanIdxPnl.wxgScanIdxPnl, gmPlugin.cPatientChange_PluginMixin):
964
984
985
986
987
989 pat = gmPerson.gmCurrentPatient()
990 if not pat.connected:
991 gmDispatcher.send(signal='statustext', msg=_('Cannot accept new documents. No active patient.'))
992 return
993
994
995 real_filenames = []
996 for pathname in filenames:
997 try:
998 files = os.listdir(pathname)
999 source = _('directory dropped on client')
1000 gmDispatcher.send(signal = 'statustext', msg = _('Extracting files from folder [%s] ...') % pathname)
1001 for filename in files:
1002 fullname = os.path.join(pathname, filename)
1003 if not os.path.isfile(fullname):
1004 continue
1005 real_filenames.append(fullname)
1006 except OSError:
1007 source = _('file dropped on client')
1008 real_filenames.append(pathname)
1009
1010 self.add_parts_from_files(real_filenames, source)
1011
1012
1015
1016
1017
1018
1022
1023
1024 - def _post_patient_selection(self, **kwds):
1025 self.__init_ui_data()
1026
1027
1028
1029
1066
1067
1076
1077
1079 title = _('saving document')
1080
1081 if self._LCTRL_doc_pages.ItemCount == 0:
1082 dbcfg = gmCfg.cCfgSQL()
1083 allow_empty = bool(dbcfg.get2 (
1084 option = 'horstspace.scan_index.allow_partless_documents',
1085 workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace,
1086 bias = 'user',
1087 default = False
1088 ))
1089 if allow_empty:
1090 save_empty = gmGuiHelpers.gm_show_question (
1091 aMessage = _('No parts to save. Really save an empty document as a reference ?'),
1092 aTitle = title
1093 )
1094 if not save_empty:
1095 return False
1096 else:
1097 gmGuiHelpers.gm_show_error (
1098 aMessage = _('No parts to save. Aquire some parts first.'),
1099 aTitle = title
1100 )
1101 return False
1102
1103 doc_type_pk = self._PhWheel_doc_type.GetData(can_create = True)
1104 if doc_type_pk is None:
1105 gmGuiHelpers.gm_show_error (
1106 aMessage = _('No document type applied. Choose a document type'),
1107 aTitle = title
1108 )
1109 return False
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119 if self._PhWheel_episode.GetValue().strip() == '':
1120 gmGuiHelpers.gm_show_error (
1121 aMessage = _('You must select an episode to save this document under.'),
1122 aTitle = title
1123 )
1124 return False
1125
1126 if self._PhWheel_reviewer.GetData() is None:
1127 gmGuiHelpers.gm_show_error (
1128 aMessage = _('You need to select from the list of staff members the doctor who is intended to sign the document.'),
1129 aTitle = title
1130 )
1131 return False
1132
1133 if self._PhWheel_doc_date.is_valid_timestamp(empty_is_valid = True) is False:
1134 gmGuiHelpers.gm_show_error (
1135 aMessage = _('Invalid date of generation.'),
1136 aTitle = title
1137 )
1138 return False
1139
1140 return True
1141
1142
1144
1145 if not reconfigure:
1146 dbcfg = gmCfg.cCfgSQL()
1147 device = dbcfg.get2 (
1148 option = 'external.xsane.default_device',
1149 workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace,
1150 bias = 'workplace',
1151 default = ''
1152 )
1153 if device.strip() == '':
1154 device = None
1155 if device is not None:
1156 return device
1157
1158 try:
1159 devices = self.scan_module.get_devices()
1160 except:
1161 _log.exception('cannot retrieve list of image sources')
1162 gmDispatcher.send(signal = 'statustext', msg = _('There is no scanner support installed on this machine.'))
1163 return None
1164
1165 if devices is None:
1166
1167
1168 return None
1169
1170 if len(devices) == 0:
1171 gmDispatcher.send(signal = 'statustext', msg = _('Cannot find an active scanner.'))
1172 return None
1173
1174
1175
1176
1177
1178 device = gmListWidgets.get_choices_from_list (
1179 parent = self,
1180 msg = _('Select an image capture device'),
1181 caption = _('device selection'),
1182 choices = [ '%s (%s)' % (d[2], d[0]) for d in devices ],
1183 columns = [_('Device')],
1184 data = devices,
1185 single_selection = True
1186 )
1187 if device is None:
1188 return None
1189
1190
1191 return device[0]
1192
1193
1194
1195
1197
1198 chosen_device = self.get_device_to_use()
1199
1200
1201
1202 try:
1203 fnames = self.scan_module.acquire_pages_into_files (
1204 device = chosen_device,
1205 delay = 5,
1206 calling_window = self
1207 )
1208 except OSError:
1209 _log.exception('problem acquiring image from source')
1210 gmGuiHelpers.gm_show_error (
1211 aMessage = _(
1212 'No pages could be acquired from the source.\n\n'
1213 'This may mean the scanner driver is not properly installed.\n\n'
1214 'On Windows you must install the TWAIN Python module\n'
1215 'while on Linux and MacOSX it is recommended to install\n'
1216 'the XSane package.'
1217 ),
1218 aTitle = _('acquiring page')
1219 )
1220 return None
1221
1222 if len(fnames) == 0:
1223 return True
1224
1225 self.add_parts_from_files(fnames, _('captured by imaging device'))
1226 return True
1227
1228
1230
1231 dlg = wx.FileDialog (
1232 parent = None,
1233 message = _('Choose a file'),
1234 defaultDir = os.path.expanduser(os.path.join('~', 'gnumed')),
1235 defaultFile = '',
1236 wildcard = "%s (*)|*|TIFFs (*.tif)|*.tif|JPEGs (*.jpg)|*.jpg|%s (*.*)|*.*" % (_('all files'), _('all files (Win)')),
1237 style = wx.FD_OPEN | wx.FD_FILE_MUST_EXIST | wx.FD_MULTIPLE
1238 )
1239 result = dlg.ShowModal()
1240 files = dlg.GetPaths()
1241 if result == wx.ID_CANCEL:
1242 dlg.Destroy()
1243 return
1244
1245 self.add_parts_from_files(files, _('picked from storage media'))
1246
1247
1256
1257
1259
1260
1261 if self._LCTRL_doc_pages.ItemCount == 0:
1262 return
1263
1264
1265 if self._LCTRL_doc_pages.ItemCount == 1:
1266 page_fnames = [ self._LCTRL_doc_pages.get_item_data(0)[0] ]
1267 else:
1268
1269 page_fnames = [ data[0] for data in self._LCTRL_doc_pages.selected_item_data ]
1270 if len(page_fnames) == 0:
1271 gmDispatcher.send(signal = 'statustext', msg = _('No part selected for viewing.'), beep = True)
1272 return
1273
1274 for page_fname in page_fnames:
1275 (success, msg) = gmMimeLib.call_viewer_on_file(page_fname)
1276 if not success:
1277 gmGuiHelpers.gm_show_warning (
1278 aMessage = _('Cannot display document part:\n%s') % msg,
1279 aTitle = _('displaying part')
1280 )
1281
1282
1284
1285 if len(self._LCTRL_doc_pages.selected_items) == 0:
1286 gmDispatcher.send(signal = 'statustext', msg = _('No part selected for removal.'), beep = True)
1287 return
1288
1289 sel_idx = self._LCTRL_doc_pages.GetFirstSelected()
1290 rows = self._LCTRL_doc_pages.string_items
1291 data = self._LCTRL_doc_pages.data
1292 del rows[sel_idx]
1293 del data[sel_idx]
1294 self._LCTRL_doc_pages.string_items = rows
1295 self._LCTRL_doc_pages.data = data
1296 self._LCTRL_doc_pages.set_column_widths()
1297 self._TCTRL_metadata.SetValue('')
1298
1299
1301
1302 if not self.__valid_for_save():
1303 return False
1304
1305
1306 cfg = gmCfg.cCfgSQL()
1307 generate_uuid = bool (
1308 cfg.get2 (
1309 option = 'horstspace.scan_index.generate_doc_uuid',
1310 workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace,
1311 bias = 'user',
1312 default = False
1313 )
1314 )
1315 if generate_uuid:
1316 ext_ref = gmDocuments.get_ext_ref()
1317 else:
1318 ext_ref = None
1319
1320
1321 date = self._PhWheel_doc_date.GetData()
1322 if date is not None:
1323 date = date.get_pydt()
1324 new_doc = save_files_as_new_document (
1325 parent = self,
1326 filenames = [ data[0] for data in self._LCTRL_doc_pages.data ],
1327 document_type = self._PhWheel_doc_type.GetValue().strip(),
1328 pk_document_type = self._PhWheel_doc_type.GetData(),
1329 unlock_patient = False,
1330 episode = self._PhWheel_episode.GetData(can_create = True, is_open = True, as_instance = True),
1331 review_as_normal = False,
1332 reference = ext_ref,
1333 pk_org_unit = self._PhWheel_source.GetData(),
1334 date_generated = date,
1335 comment = self._PRW_doc_comment.GetLineText(0).strip(),
1336 reviewer = self._PhWheel_reviewer.GetData()
1337 )
1338 if new_doc is None:
1339 return False
1340
1341 if self._RBTN_org_is_receiver.Value is True:
1342 new_doc['unit_is_receiver'] = True
1343 new_doc.save()
1344
1345
1346 description = self._TBOX_description.GetValue().strip()
1347 if description != '':
1348 if not new_doc.add_description(description):
1349 wx.EndBusyCursor()
1350 gmGuiHelpers.gm_show_error (
1351 aMessage = _('Cannot add document description.'),
1352 aTitle = _('saving document')
1353 )
1354 return False
1355
1356
1357 if self._ChBOX_reviewed.GetValue():
1358 if not new_doc.set_reviewed (
1359 technically_abnormal = self._ChBOX_abnormal.GetValue(),
1360 clinically_relevant = self._ChBOX_relevant.GetValue()
1361 ):
1362 msg = _('Error setting "reviewed" status of new document.')
1363
1364 self.__init_ui_data()
1365
1366 gmHooks.run_hook_script(hook = 'after_new_doc_created')
1367
1368 return True
1369
1370
1372 self.__init_ui_data()
1373
1374
1376 self._ChBOX_abnormal.Enable(enable = self._ChBOX_reviewed.GetValue())
1377 self._ChBOX_relevant.Enable(enable = self._ChBOX_reviewed.GetValue())
1378
1379
1381 pk_doc_type = self._PhWheel_doc_type.GetData()
1382 if pk_doc_type is None:
1383 self._PRW_doc_comment.unset_context(context = 'pk_doc_type')
1384 else:
1385 self._PRW_doc_comment.set_context(context = 'pk_doc_type', val = pk_doc_type)
1386 return True
1387
1388
1390 status, description = result
1391 fname, source = self._LCTRL_doc_pages.get_selected_item_data(only_one = True)
1392 txt = _(
1393 'Source: %s\n'
1394 'File: %s\n'
1395 '\n'
1396 '%s'
1397 ) % (
1398 source,
1399 fname,
1400 description
1401 )
1402 wx.CallAfter(self._TCTRL_metadata.SetValue, txt)
1403
1404
1410
1411
1413
1414 if parent is None:
1415 parent = wx.GetApp().GetTopWindow()
1416
1417
1418 if part['size'] == 0:
1419 _log.debug('cannot display part [%s] - 0 bytes', part['pk_obj'])
1420 gmGuiHelpers.gm_show_error (
1421 aMessage = _('Document part does not seem to exist in database !'),
1422 aTitle = _('showing document')
1423 )
1424 return None
1425
1426 wx.BeginBusyCursor()
1427 cfg = gmCfg.cCfgSQL()
1428
1429
1430 chunksize = int(
1431 cfg.get2 (
1432 option = "horstspace.blob_export_chunk_size",
1433 workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace,
1434 bias = 'workplace',
1435 default = 2048
1436 ))
1437
1438
1439 block_during_view = bool( cfg.get2 (
1440 option = 'horstspace.document_viewer.block_during_view',
1441 workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace,
1442 bias = 'user',
1443 default = None
1444 ))
1445
1446 wx.EndBusyCursor()
1447
1448
1449 successful, msg = part.display_via_mime (
1450 chunksize = chunksize,
1451 block = block_during_view
1452 )
1453 if not successful:
1454 gmGuiHelpers.gm_show_error (
1455 aMessage = _('Cannot display document part:\n%s') % msg,
1456 aTitle = _('showing document')
1457 )
1458 return None
1459
1460
1461
1462
1463
1464
1465
1466 review_after_display = int(cfg.get2 (
1467 option = 'horstspace.document_viewer.review_after_display',
1468 workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace,
1469 bias = 'user',
1470 default = 3
1471 ))
1472 if review_after_display == 1:
1473 review_document_part(parent = parent, part = part)
1474 elif review_after_display == 2:
1475 review_by_me = [ rev for rev in part.get_reviews() if rev['is_your_review'] ]
1476 if len(review_by_me) == 0:
1477 review_document_part(parent = parent, part = part)
1478 elif review_after_display == 3:
1479 if len(part.get_reviews()) == 0:
1480 review_document_part(parent = parent, part = part)
1481 elif review_after_display == 4:
1482 reviewed_by_responsible = [ rev for rev in part.get_reviews() if rev['is_review_by_responsible_reviewer'] ]
1483 if len(reviewed_by_responsible) == 0:
1484 review_document_part(parent = parent, part = part)
1485
1486 return True
1487
1488
1489 -def manage_documents(parent=None, msg=None, single_selection=True, pk_types=None, pk_episodes=None):
1499
1500
1501
1502 def delete(document):
1503 return
1504
1505
1506
1507
1508
1509
1510
1511 def refresh(lctrl):
1512 docs = pat.document_folder.get_documents(pk_types = pk_types, pk_episodes = pk_episodes)
1513 items = [ [
1514 gmDateTime.pydt_strftime(d['clin_when'], '%Y %b %d', accuracy = gmDateTime.acc_days),
1515 d['l10n_type'],
1516 gmTools.coalesce(d['comment'], ''),
1517 gmTools.coalesce(d['ext_ref'], ''),
1518 d['pk_doc']
1519 ] for d in docs ]
1520 lctrl.set_string_items(items)
1521 lctrl.set_data(docs)
1522
1523
1524 def show_doc(doc):
1525 if doc is None:
1526 return
1527 for fname in doc.save_parts_to_files():
1528 gmMimeLib.call_viewer_on_file(aFile = fname, block = False)
1529
1530
1531 return gmListWidgets.get_choices_from_list (
1532 parent = parent,
1533 caption = _('Patient document list'),
1534 columns = [_('Generated'), _('Type'), _('Comment'), _('Ref #'), '#'],
1535 single_selection = single_selection,
1536
1537
1538
1539 refresh_callback = refresh,
1540 left_extra_button = (_('Show'), _('Show all parts of this document in external viewer.'), show_doc)
1541 )
1542
1543
1544 from Gnumed.wxGladeWidgets import wxgSelectablySortedDocTreePnl
1545
1547 """A panel with a document tree which can be sorted."""
1548
1549 - def __init__(self, parent, id, *args, **kwds):
1555
1556
1557
1558
1563
1564
1569
1570
1575
1580
1585
1590
1591
1592 - def _update_details(self, issue=None, episode=None, org_unit=None, document=None, part=None):
1593
1594 self._LCTRL_details.set_string_items([])
1595
1596 if document is None:
1597 if part is not None:
1598 document = part.document
1599
1600 if issue is None:
1601 if episode is not None:
1602 issue = episode.health_issue
1603
1604 items = []
1605
1606 if issue is not None:
1607 items.append([_('Health issue'), '%s%s [#%s]' % (
1608 issue['description'],
1609 gmTools.coalesce (
1610 initial = issue['laterality'],
1611 instead = '',
1612 template_initial = ' (%s)',
1613 none_equivalents = [None, '', '?']
1614 ),
1615 issue['pk_health_issue']
1616 )])
1617 items.append([_('Status'), '%s, %s %s' % (
1618 gmTools.bool2subst(issue['is_active'], _('active'), _('inactive')),
1619 gmTools.bool2subst(issue['clinically_relevant'], _('clinically relevant'), _('not clinically relevant')),
1620 issue.diagnostic_certainty_description
1621 )])
1622 items.append([_('Confidential'), issue['is_confidential']])
1623 items.append([_('Age noted'), issue.age_noted_human_readable()])
1624
1625 if episode is not None:
1626 items.append([_('Episode'), '%s [#%s]' % (
1627 episode['description'],
1628 episode['pk_episode']
1629 )])
1630 items.append([_('Status'), '%s %s' % (
1631 gmTools.bool2subst(episode['episode_open'], _('active'), _('finished')),
1632 episode.diagnostic_certainty_description
1633 )])
1634 items.append([_('Health issue'), gmTools.coalesce(episode['health_issue'], '')])
1635
1636 if org_unit is not None:
1637 items.append([_('Organization'), '%s (%s) [#%s]' % (
1638 org_unit['organization'],
1639 org_unit['l10n_organization_category'],
1640 org_unit['pk_org']
1641 )])
1642 items.append([_('Department'), '%s%s [#%s]' % (
1643 org_unit['unit'],
1644 gmTools.coalesce(org_unit['l10n_unit_category'], '', ' (%s)'),
1645 org_unit['pk_org_unit']
1646 )])
1647 adr = org_unit.address
1648 if adr is not None:
1649 lines = adr.format()
1650 items.append([lines[0], lines[1]])
1651 for line in lines[2:]:
1652 items.append(['', line])
1653 for comm in org_unit.comm_channels:
1654 items.append([comm['l10n_comm_type'], '%s%s' % (
1655 comm['url'],
1656 gmTools.bool2subst(comm['is_confidential'], _(' (confidential)'), '', '')
1657 )])
1658
1659 if document is not None:
1660 items.append([_('Document'), '%s [#%s]' % (document['l10n_type'], document['pk_doc'])])
1661 items.append([_('Generated'), gmDateTime.pydt_strftime(document['clin_when'], '%Y %b %d')])
1662 items.append([_('Health issue'), gmTools.coalesce(document['health_issue'], '', '%%s [#%s]' % document['pk_health_issue'])])
1663 items.append([_('Episode'), '%s (%s) [#%s]' % (
1664 document['episode'],
1665 gmTools.bool2subst(document['episode_open'], _('open'), _('closed')),
1666 document['pk_episode']
1667 )])
1668 if document['pk_org_unit'] is not None:
1669 if document['unit_is_receiver']:
1670 header = _('Receiver')
1671 else:
1672 header = _('Sender')
1673 items.append([header, '%s @ %s' % (document['unit'], document['organization'])])
1674 if document['ext_ref'] is not None:
1675 items.append([_('Reference'), document['ext_ref']])
1676 if document['comment'] is not None:
1677 items.append([_('Comment'), ' / '.join(document['comment'].split('\n'))])
1678 for proc in document.procedures:
1679 items.append([_('Procedure'), proc.format (
1680 left_margin = 0,
1681 include_episode = False,
1682 include_codes = False,
1683 include_address = False,
1684 include_comm = False,
1685 include_doc = False
1686 )])
1687 stay = document.hospital_stay
1688 if stay is not None:
1689 items.append([_('Hospital stay'), stay.format(include_episode = False)])
1690 for bill in document.bills:
1691 items.append([_('Bill'), bill.format (
1692 include_receiver = False,
1693 include_doc = False
1694 )])
1695 items.append([_('Modified'), gmDateTime.pydt_strftime(document['modified_when'], '%Y %b %d')])
1696 items.append([_('... by'), document['modified_by']])
1697 items.append([_('# encounter'), document['pk_encounter']])
1698
1699 if part is not None:
1700 items.append(['', ''])
1701 if part['seq_idx'] is None:
1702 items.append([_('Part'), '#%s' % part['pk_obj']])
1703 else:
1704 items.append([_('Part'), '%s [#%s]' % (part['seq_idx'], part['pk_obj'])])
1705 if part['obj_comment'] is not None:
1706 items.append([_('Comment'), part['obj_comment']])
1707 if part['filename'] is not None:
1708 items.append([_('Filename'), part['filename']])
1709 items.append([_('Data size'), gmTools.size2str(part['size'])])
1710 review_parts = []
1711 if part['reviewed_by_you']:
1712 review_parts.append(_('by you'))
1713 if part['reviewed_by_intended_reviewer']:
1714 review_parts.append(_('by intended reviewer'))
1715 review = ', '.join(review_parts)
1716 if review == '':
1717 review = gmTools.u_diameter
1718 items.append([_('Reviewed'), review])
1719
1720
1721 self._LCTRL_details.set_string_items(items)
1722 self._LCTRL_details.set_column_widths()
1723 self._LCTRL_details.set_resize_column(1)
1724
1725
1726 -class cDocTree(wx.TreeCtrl, gmRegetMixin.cRegetOnPaintMixin, treemixin.ExpansionState):
1727 """This wx.TreeCtrl derivative displays a tree view of stored medical documents.
1728
1729 It listens to document and patient changes and updates itself accordingly.
1730
1731 This acts on the current patient.
1732 """
1733 _sort_modes = ['age', 'review', 'episode', 'type', 'issue', 'org']
1734 _root_node_labels = None
1735
1736
1737 - def __init__(self, parent, id, *args, **kwds):
1738 """Set up our specialised tree.
1739 """
1740 kwds['style'] = wx.TR_NO_BUTTONS | wx.NO_BORDER | wx.TR_SINGLE
1741 wx.TreeCtrl.__init__(self, parent, id, *args, **kwds)
1742
1743 gmRegetMixin.cRegetOnPaintMixin.__init__(self)
1744
1745 tmp = _('available documents (%s)')
1746 unsigned = _('unsigned (%s) on top') % '\u270D'
1747 cDocTree._root_node_labels = {
1748 'age': tmp % _('most recent on top'),
1749 'review': tmp % unsigned,
1750 'episode': tmp % _('sorted by episode'),
1751 'issue': tmp % _('sorted by health issue'),
1752 'type': tmp % _('sorted by type'),
1753 'org': tmp % _('sorted by organization')
1754 }
1755
1756 self.root = None
1757 self.__sort_mode = 'age'
1758
1759 self.__expanded_nodes = None
1760 self.__show_details_callback = None
1761
1762 self.__build_context_menus()
1763 self.__register_interests()
1764 self._schedule_data_reget()
1765
1766
1767
1768
1770
1771 node = self.GetSelection()
1772 node_data = self.GetItemData(node)
1773
1774 if not isinstance(node_data, gmDocuments.cDocumentPart):
1775 return True
1776
1777 self.__display_part(part = node_data)
1778 return True
1779
1780
1781
1782
1784 return self.__sort_mode
1785
1804
1805 sort_mode = property(_get_sort_mode, _set_sort_mode)
1806
1807
1809 if callback is not None:
1810 if not callable(callback):
1811 raise ValueError('<%s> is not callable')
1812 self.__show_details_callback = callback
1813
1814 show_details_callback = property(lambda x:x, _set_show_details_callback)
1815
1816
1817
1818
1820 curr_pat = gmPerson.gmCurrentPatient()
1821 if not curr_pat.connected:
1822 gmDispatcher.send(signal = 'statustext', msg = _('Cannot load documents. No active patient.'))
1823 return False
1824
1825 if not self.__populate_tree():
1826 return False
1827
1828 return True
1829
1830
1831
1832
1834
1835 self.Bind(wx.EVT_TREE_SEL_CHANGED, self._on_tree_item_selected)
1836 self.Bind(wx.EVT_TREE_ITEM_ACTIVATED, self._on_activate)
1837 self.Bind(wx.EVT_TREE_ITEM_RIGHT_CLICK, self.__on_right_click)
1838 self.Bind(wx.EVT_TREE_ITEM_GETTOOLTIP, self._on_tree_item_gettooltip)
1839
1840
1841
1842
1843
1844
1845
1846 gmDispatcher.connect(signal = 'pre_patient_unselection', receiver = self._on_pre_patient_unselection)
1847 gmDispatcher.connect(signal = 'post_patient_selection', receiver = self._on_post_patient_selection)
1848 gmDispatcher.connect(signal = 'blobs.doc_med_mod_db', receiver = self._on_doc_mod_db)
1849 gmDispatcher.connect(signal = 'blobs.doc_obj_mod_db', receiver = self._on_doc_page_mod_db)
1850
1851
1853
1854
1855 self.__part_context_menu = wx.Menu(title = _('Part Actions:'))
1856
1857 item = self.__part_context_menu.Append(-1, _('Display part'))
1858 self.Bind(wx.EVT_MENU, self.__display_curr_part, item)
1859 item = self.__part_context_menu.Append(-1, _('%s Sign/Edit properties') % '\u270D')
1860 self.Bind(wx.EVT_MENU, self.__review_curr_part, item)
1861
1862 self.__part_context_menu.AppendSeparator()
1863
1864 item = self.__part_context_menu.Append(-1, _('Delete part'))
1865 self.Bind(wx.EVT_MENU, self.__delete_part, item, item)
1866 item = self.__part_context_menu.Append(-1, _('Move part'))
1867 self.Bind(wx.EVT_MENU, self.__move_part, item)
1868 item = self.__part_context_menu.Append(-1, _('Print part'))
1869 self.Bind(wx.EVT_MENU, self.__print_part, item)
1870 item = self.__part_context_menu.Append(-1, _('Fax part'))
1871 self.Bind(wx.EVT_MENU, self.__fax_part, item)
1872 item = self.__part_context_menu.Append(-1, _('Mail part'))
1873 self.Bind(wx.EVT_MENU, self.__mail_part, item)
1874 item = self.__part_context_menu.Append(-1, _('Save part to disk'))
1875 self.Bind(wx.EVT_MENU, self.__save_part_to_disk, item)
1876
1877 self.__part_context_menu.AppendSeparator()
1878
1879
1880 self.__doc_context_menu = wx.Menu(title = _('Document Actions:'))
1881
1882 item = self.__doc_context_menu.Append(-1, _('%s Sign/Edit properties') % '\u270D')
1883 self.Bind(wx.EVT_MENU, self.__review_curr_part, item)
1884 item = self.__doc_context_menu.Append(-1, _('Delete document'))
1885 self.Bind(wx.EVT_MENU, self.__delete_document, item)
1886
1887 self.__doc_context_menu.AppendSeparator()
1888
1889 item = self.__doc_context_menu.Append(-1, _('Add parts'))
1890 self.Bind(wx.EVT_MENU, self.__add_part, item)
1891 item = self.__doc_context_menu.Append(-1, _('Add part from clipboard'))
1892 self.Bind(wx.EVT_MENU, self.__add_part_from_clipboard, item, item)
1893 item = self.__doc_context_menu.Append(-1, _('Print all parts'))
1894 self.Bind(wx.EVT_MENU, self.__print_doc, item)
1895 item = self.__doc_context_menu.Append(-1, _('Fax all parts'))
1896 self.Bind(wx.EVT_MENU, self.__fax_doc, item)
1897 item = self.__doc_context_menu.Append(-1, _('Mail all parts'))
1898 self.Bind(wx.EVT_MENU, self.__mail_doc, item)
1899 item = self.__doc_context_menu.Append(-1, _('Save all parts to disk'))
1900 self.Bind(wx.EVT_MENU, self.__save_doc_to_disk, item)
1901 item = self.__doc_context_menu.Append(-1, _('Copy all parts to export area'))
1902 self.Bind(wx.EVT_MENU, self.__copy_doc_to_export_area, item)
1903
1904 self.__doc_context_menu.AppendSeparator()
1905
1906 item = self.__doc_context_menu.Append(-1, _('Access external original'))
1907 self.Bind(wx.EVT_MENU, self.__access_external_original, item)
1908 item = self.__doc_context_menu.Append(-1, _('Edit corresponding encounter'))
1909 self.Bind(wx.EVT_MENU, self.__edit_encounter_details, item)
1910 item = self.__doc_context_menu.Append(-1, _('Select corresponding encounter'))
1911 self.Bind(wx.EVT_MENU, self.__select_encounter, item)
1912 item = self.__doc_context_menu.Append(-1, _('Manage descriptions'))
1913 self.Bind(wx.EVT_MENU, self.__manage_document_descriptions, item)
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1926
1927 wx.BeginBusyCursor()
1928
1929
1930 if self.root is not None:
1931 self.DeleteAllItems()
1932
1933
1934 self.root = self.AddRoot(cDocTree._root_node_labels[self.__sort_mode], -1, -1)
1935 self.SetItemData(self.root, None)
1936 self.SetItemHasChildren(self.root, False)
1937
1938
1939 curr_pat = gmPerson.gmCurrentPatient()
1940 docs_folder = curr_pat.get_document_folder()
1941 docs = docs_folder.get_documents()
1942
1943 if docs is None:
1944 gmGuiHelpers.gm_show_error (
1945 aMessage = _('Error searching documents.'),
1946 aTitle = _('loading document list')
1947 )
1948
1949 wx.EndBusyCursor()
1950 return True
1951
1952 if len(docs) == 0:
1953 wx.EndBusyCursor()
1954 return True
1955
1956
1957 self.SetItemHasChildren(self.root, True)
1958
1959
1960 intermediate_nodes = {}
1961 for doc in docs:
1962
1963 parts = doc.parts
1964
1965 if len(parts) == 0:
1966 no_parts = _('no parts')
1967 elif len(parts) == 1:
1968 no_parts = _('1 part')
1969 else:
1970 no_parts = _('%s parts') % len(parts)
1971
1972
1973 if self.__sort_mode == 'episode':
1974 intermediate_label = '%s%s' % (doc['episode'], gmTools.coalesce(doc['health_issue'], '', ' (%s)'))
1975 doc_label = _('%s%7s %s:%s (%s)') % (
1976 gmTools.bool2subst(doc.has_unreviewed_parts, gmTools.u_writing_hand, '', '?'),
1977 doc['clin_when'].strftime('%m/%Y'),
1978 doc['l10n_type'][:26],
1979 gmTools.coalesce(initial = doc['comment'], instead = '', template_initial = ' %s'),
1980 no_parts
1981 )
1982 if intermediate_label not in intermediate_nodes:
1983 intermediate_nodes[intermediate_label] = self.AppendItem(parent = self.root, text = intermediate_label)
1984 self.SetItemBold(intermediate_nodes[intermediate_label], bold = True)
1985 self.SetItemData(intermediate_nodes[intermediate_label], {'pk_episode': doc['pk_episode']})
1986 self.SetItemHasChildren(intermediate_nodes[intermediate_label], True)
1987 parent = intermediate_nodes[intermediate_label]
1988
1989 elif self.__sort_mode == 'type':
1990 intermediate_label = doc['l10n_type']
1991 doc_label = _('%s%7s (%s):%s') % (
1992 gmTools.bool2subst(doc.has_unreviewed_parts, gmTools.u_writing_hand, '', '?'),
1993 doc['clin_when'].strftime('%m/%Y'),
1994 no_parts,
1995 gmTools.coalesce(initial = doc['comment'], instead = '', template_initial = ' %s')
1996 )
1997 if intermediate_label not in intermediate_nodes:
1998 intermediate_nodes[intermediate_label] = self.AppendItem(parent = self.root, text = intermediate_label)
1999 self.SetItemBold(intermediate_nodes[intermediate_label], bold = True)
2000 self.SetItemData(intermediate_nodes[intermediate_label], None)
2001 self.SetItemHasChildren(intermediate_nodes[intermediate_label], True)
2002 parent = intermediate_nodes[intermediate_label]
2003
2004 elif self.__sort_mode == 'issue':
2005 if doc['health_issue'] is None:
2006 intermediate_label = _('%s (unattributed episode)') % doc['episode']
2007 else:
2008 intermediate_label = doc['health_issue']
2009 doc_label = _('%s%7s %s:%s (%s)') % (
2010 gmTools.bool2subst(doc.has_unreviewed_parts, gmTools.u_writing_hand, '', '?'),
2011 doc['clin_when'].strftime('%m/%Y'),
2012 doc['l10n_type'][:26],
2013 gmTools.coalesce(initial = doc['comment'], instead = '', template_initial = ' %s'),
2014 no_parts
2015 )
2016 if intermediate_label not in intermediate_nodes:
2017 intermediate_nodes[intermediate_label] = self.AppendItem(parent = self.root, text = intermediate_label)
2018 self.SetItemBold(intermediate_nodes[intermediate_label], bold = True)
2019 self.SetItemData(intermediate_nodes[intermediate_label], {'pk_health_issue': doc['pk_health_issue']})
2020 self.SetItemHasChildren(intermediate_nodes[intermediate_label], True)
2021 parent = intermediate_nodes[intermediate_label]
2022
2023 elif self.__sort_mode == 'org':
2024 if doc['pk_org'] is None:
2025 intermediate_label = _('unknown organization')
2026 else:
2027 if doc['unit_is_receiver']:
2028 direction = _('to: %s')
2029 else:
2030 direction = _('from: %s')
2031
2032 if doc['pk_org'] == gmPraxis.gmCurrentPraxisBranch()['pk_org']:
2033 org_str = _('this praxis')
2034 else:
2035 org_str = doc['organization']
2036 intermediate_label = direction % org_str
2037 doc_label = _('%s%7s %s:%s (%s)') % (
2038 gmTools.bool2subst(doc.has_unreviewed_parts, gmTools.u_writing_hand, '', '?'),
2039 doc['clin_when'].strftime('%m/%Y'),
2040 doc['l10n_type'][:26],
2041 gmTools.coalesce(initial = doc['comment'], instead = '', template_initial = ' %s'),
2042 no_parts
2043 )
2044 if intermediate_label not in intermediate_nodes:
2045 intermediate_nodes[intermediate_label] = self.AppendItem(parent = self.root, text = intermediate_label)
2046 self.SetItemBold(intermediate_nodes[intermediate_label], bold = True)
2047
2048
2049
2050 self.SetItemData(intermediate_nodes[intermediate_label], doc.org_unit)
2051 self.SetItemHasChildren(intermediate_nodes[intermediate_label], True)
2052 parent = intermediate_nodes[intermediate_label]
2053
2054 else:
2055 doc_label = _('%s%7s %s:%s (%s)') % (
2056 gmTools.bool2subst(doc.has_unreviewed_parts, gmTools.u_writing_hand, '', '?'),
2057 doc['clin_when'].strftime('%Y-%m'),
2058 doc['l10n_type'][:26],
2059 gmTools.coalesce(initial = doc['comment'], instead = '', template_initial = ' %s'),
2060 no_parts
2061 )
2062 parent = self.root
2063
2064 doc_node = self.AppendItem(parent = parent, text = doc_label)
2065
2066 self.SetItemData(doc_node, doc)
2067 if len(parts) == 0:
2068 self.SetItemHasChildren(doc_node, False)
2069 else:
2070 self.SetItemHasChildren(doc_node, True)
2071
2072
2073 for part in parts:
2074 f_ext = ''
2075 if part['filename'] is not None:
2076 f_ext = os.path.splitext(part['filename'])[1].strip('.').strip()
2077 if f_ext != '':
2078 f_ext = ' .' + f_ext.upper()
2079 label = '%s%s (%s%s)%s' % (
2080 gmTools.bool2str (
2081 boolean = part['reviewed'] or part['reviewed_by_you'] or part['reviewed_by_intended_reviewer'],
2082 true_str = '',
2083 false_str = gmTools.u_writing_hand
2084 ),
2085 _('part %2s') % part['seq_idx'],
2086 gmTools.size2str(part['size']),
2087 f_ext,
2088 gmTools.coalesce (
2089 part['obj_comment'],
2090 '',
2091 ': %s%%s%s' % (gmTools.u_left_double_angle_quote, gmTools.u_right_double_angle_quote)
2092 )
2093 )
2094
2095 part_node = self.AppendItem(parent = doc_node, text = label)
2096 self.SetItemData(part_node, part)
2097 self.SetItemHasChildren(part_node, False)
2098
2099 self.__sort_nodes()
2100 self.SelectItem(self.root)
2101
2102
2103 if self.__expanded_nodes is not None:
2104 self.ExpansionState = self.__expanded_nodes
2105
2106 self.Expand(self.root)
2107
2108
2109 if self.__expanded_nodes is None:
2110
2111 if self.__sort_mode in ['episode', 'type', 'issue', 'org']:
2112 for key in intermediate_nodes.keys():
2113 self.Expand(intermediate_nodes[key])
2114
2115 wx.EndBusyCursor()
2116
2117 return True
2118
2119
2121 """Used in sorting items.
2122
2123 -1: 1 < 2
2124 0: 1 = 2
2125 1: 1 > 2
2126 """
2127
2128 if not node1:
2129 _log.debug('invalid node 1')
2130 return 0
2131 if not node2:
2132 _log.debug('invalid node 2')
2133 return 0
2134 if not node1.IsOk():
2135 _log.debug('no data on node 1')
2136 return 0
2137 if not node2.IsOk():
2138 _log.debug('no data on node 2')
2139 return 0
2140
2141 data1 = self.GetItemData(node1)
2142 data2 = self.GetItemData(node2)
2143
2144
2145 if isinstance(data1, gmDocuments.cDocument):
2146 date_field = 'clin_when'
2147
2148 if self.__sort_mode == 'age':
2149
2150 if data1[date_field] > data2[date_field]:
2151 return -1
2152 if data1[date_field] == data2[date_field]:
2153 return 0
2154 return 1
2155 if self.__sort_mode == 'episode':
2156 if data1['episode'] < data2['episode']:
2157 return -1
2158 if data1['episode'] == data2['episode']:
2159
2160 if data1[date_field] > data2[date_field]:
2161 return -1
2162 if data1[date_field] == data2[date_field]:
2163 return 0
2164 return 1
2165 return 1
2166 if self.__sort_mode == 'issue':
2167 if data1['health_issue'] == data2['health_issue']:
2168
2169 if data1[date_field] > data2[date_field]:
2170 return -1
2171 if data1[date_field] == data2[date_field]:
2172 return 0
2173 return 1
2174 if data1['health_issue'] < data2['health_issue']:
2175 return -1
2176 return 1
2177 if self.__sort_mode == 'review':
2178
2179 if data1.has_unreviewed_parts == data2.has_unreviewed_parts:
2180
2181 if data1[date_field] > data2[date_field]:
2182 return -1
2183 if data1[date_field] == data2[date_field]:
2184 return 0
2185 return 1
2186 if data1.has_unreviewed_parts:
2187 return -1
2188 return 1
2189 if self.__sort_mode == 'type':
2190 if data1['l10n_type'] < data2['l10n_type']:
2191 return -1
2192 if data1['l10n_type'] == data2['l10n_type']:
2193
2194 if data1[date_field] > data2[date_field]:
2195 return -1
2196 if data1[date_field] == data2[date_field]:
2197 return 0
2198 return 1
2199 return 1
2200 if self.__sort_mode == 'org':
2201 if (data1['organization'] is None) and (data2['organization'] is None):
2202 return 0
2203 if (data1['organization'] is None) and (data2['organization'] is not None):
2204 return 1
2205 if (data1['organization'] is not None) and (data2['organization'] is None):
2206 return -1
2207 txt1 = '%s %s' % (data1['organization'], data1['unit'])
2208 txt2 = '%s %s' % (data2['organization'], data2['unit'])
2209 if txt1 < txt2:
2210 return -1
2211 if txt1 == txt2:
2212
2213 if data1[date_field] > data2[date_field]:
2214 return -1
2215 if data1[date_field] == data2[date_field]:
2216 return 0
2217 return 1
2218 return 1
2219
2220 _log.error('unknown document sort mode [%s], reverse-sorting by age', self.__sort_mode)
2221
2222 if data1[date_field] > data2[date_field]:
2223 return -1
2224 if data1[date_field] == data2[date_field]:
2225 return 0
2226 return 1
2227
2228
2229 if isinstance(data1, gmDocuments.cDocumentPart):
2230
2231
2232 if data1['seq_idx'] < data2['seq_idx']:
2233 return -1
2234 if data1['seq_idx'] == data2['seq_idx']:
2235 return 0
2236 return 1
2237
2238
2239 if isinstance(data1, gmOrganization.cOrgUnit):
2240 l1 = self.GetItemText(node1)
2241 l2 = self.GetItemText(node2)
2242 if l1 < l2:
2243 return -1
2244 if l1 == l2:
2245 return 0
2246 return 1
2247
2248
2249 if isinstance(data1, dict):
2250 if ('pk_episode' in data1) or ('pk_health_issue' in data1):
2251 l1 = self.GetItemText(node1)
2252 l2 = self.GetItemText(node2)
2253 if l1 < l2:
2254 return -1
2255 if l1 == l2:
2256 return 0
2257 return 1
2258 _log.error('dict but unknown content: %s', data1.keys())
2259 return 1
2260
2261
2262
2263 if None in [data1, data2]:
2264 l1 = self.GetItemText(node1)
2265 l2 = self.GetItemText(node2)
2266 if l1 < l2:
2267 return -1
2268 if l1 == l2:
2269 return 0
2270 else:
2271 if data1 < data2:
2272 return -1
2273 if data1 == data2:
2274 return 0
2275 return 1
2276
2277
2278
2279
2281 self.__expanded_nodes = self.ExpansionState
2282 self._schedule_data_reget()
2283
2284 - def _on_doc_page_mod_db(self, *args, **kwargs):
2285 self.__expanded_nodes = self.ExpansionState
2286 self._schedule_data_reget()
2287
2289
2290 if self.root is not None:
2291 self.DeleteAllItems()
2292 self.root = None
2293
2294 - def _on_post_patient_selection(self, *args, **kwargs):
2295
2296 self.__expanded_nodes = None
2297 self._schedule_data_reget()
2298
2299
2301 node = event.GetItem()
2302 node_data = self.GetItemData(node)
2303
2304
2305 if node_data is None:
2306 self.__show_details_callback(document = None, part = None)
2307 return
2308
2309
2310 if isinstance(node_data, gmDocuments.cDocument):
2311 self.__show_details_callback(document = node_data, part = None)
2312 return
2313
2314 if isinstance(node_data, gmDocuments.cDocumentPart):
2315 doc = self.GetItemData(self.GetItemParent(node))
2316 self.__show_details_callback(document = doc, part = node_data)
2317 return
2318
2319 if isinstance(node_data, gmOrganization.cOrgUnit):
2320 self.__show_details_callback(org_unit = node_data)
2321 return
2322
2323 if isinstance(node_data, dict):
2324 _log.debug('node data is dict: %s', node_data)
2325 issue = None
2326 try:
2327 if node_data['pk_health_issue'] is None:
2328 _log.debug('node data dict holds pseudo-issue for unattributed episodes, ignoring')
2329 else:
2330 issue = gmEMRStructItems.cHealthIssue(aPK_obj = node_data['pk_health_issue'])
2331 except KeyError:
2332 pass
2333 episode = None
2334 try:
2335 epi = gmEMRStructItems.cEpisode(aPK_obj = node_data['pk_episode'])
2336 except KeyError:
2337 pass
2338 self.__show_details_callback(issue = issue, episode = epi)
2339 return
2340
2341
2342
2343
2344
2345
2346 raise ValueError('invalid document tree node data type: %s' % type(node_data))
2347
2348
2350 node = event.GetItem()
2351 node_data = self.GetItemData(node)
2352
2353
2354 if node_data is None:
2355 return None
2356
2357
2358 if isinstance(node_data, gmDocuments.cDocument):
2359 self.Toggle(node)
2360 return True
2361
2362
2363 if isinstance(node_data, str):
2364 self.Toggle(node)
2365 return True
2366
2367 if isinstance(node_data, gmDocuments.cDocumentPart):
2368 self.__display_part(part = node_data)
2369 return True
2370
2371 raise ValueError(_('invalid document tree node data type: %s') % type(node_data))
2372
2373
2375
2376 node = evt.GetItem()
2377 self.__curr_node_data = self.GetItemData(node)
2378
2379
2380 if self.__curr_node_data is None:
2381 return None
2382
2383
2384 if isinstance(self.__curr_node_data, gmDocuments.cDocument):
2385 self.__handle_doc_context()
2386
2387
2388 if isinstance(self.__curr_node_data, gmDocuments.cDocumentPart):
2389 self.__handle_part_context()
2390
2391 del self.__curr_node_data
2392 evt.Skip()
2393
2394
2397
2399 self.__display_part(part = self.__curr_node_data)
2400
2402 self.__review_part(part = self.__curr_node_data)
2403
2406
2434
2435
2436
2437
2439
2440 if start_node is None:
2441 start_node = self.GetRootItem()
2442
2443
2444
2445 if not start_node.IsOk():
2446 return True
2447
2448 self.SortChildren(start_node)
2449
2450 child_node, cookie = self.GetFirstChild(start_node)
2451 while child_node.IsOk():
2452 self.__sort_nodes(start_node = child_node)
2453 child_node, cookie = self.GetNextChild(start_node, cookie)
2454
2455 return
2456
2458 self.PopupMenu(self.__doc_context_menu, wx.DefaultPosition)
2459
2460
2462 ID = None
2463
2464 if self.__curr_node_data['type'] == 'patient photograph':
2465 item = self.__part_context_menu.Append(-1, _('Activate as current photo'))
2466 self.Bind(wx.EVT_MENU, self.__activate_as_current_photo, item)
2467 ID = item.Id
2468
2469 self.PopupMenu(self.__part_context_menu, wx.DefaultPosition)
2470
2471 if ID is not None:
2472 self.__part_context_menu.Delete(ID)
2473
2474
2475
2476
2478 """Display document part."""
2479
2480
2481 if part['size'] == 0:
2482 _log.debug('cannot display part [%s] - 0 bytes', part['pk_obj'])
2483 gmGuiHelpers.gm_show_error (
2484 aMessage = _('Document part does not seem to exist in database !'),
2485 aTitle = _('showing document')
2486 )
2487 return None
2488
2489 wx.BeginBusyCursor()
2490
2491 cfg = gmCfg.cCfgSQL()
2492
2493
2494 chunksize = int(
2495 cfg.get2 (
2496 option = "horstspace.blob_export_chunk_size",
2497 workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace,
2498 bias = 'workplace',
2499 default = default_chunksize
2500 ))
2501
2502
2503 block_during_view = bool( cfg.get2 (
2504 option = 'horstspace.document_viewer.block_during_view',
2505 workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace,
2506 bias = 'user',
2507 default = None
2508 ))
2509
2510
2511 successful, msg = part.display_via_mime (
2512 chunksize = chunksize,
2513 block = block_during_view
2514 )
2515
2516 wx.EndBusyCursor()
2517
2518 if not successful:
2519 gmGuiHelpers.gm_show_error (
2520 aMessage = _('Cannot display document part:\n%s') % msg,
2521 aTitle = _('showing document')
2522 )
2523 return None
2524
2525
2526
2527
2528
2529
2530
2531 review_after_display = int(cfg.get2 (
2532 option = 'horstspace.document_viewer.review_after_display',
2533 workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace,
2534 bias = 'user',
2535 default = 3
2536 ))
2537 if review_after_display == 1:
2538 self.__review_part(part=part)
2539 elif review_after_display == 2:
2540 review_by_me = [ rev for rev in part.get_reviews() if rev['is_your_review'] ]
2541 if len(review_by_me) == 0:
2542 self.__review_part(part = part)
2543 elif review_after_display == 3:
2544 if len(part.get_reviews()) == 0:
2545 self.__review_part(part = part)
2546 elif review_after_display == 4:
2547 reviewed_by_responsible = [ rev for rev in part.get_reviews() if rev['is_review_by_responsible_reviewer'] ]
2548 if len(reviewed_by_responsible) == 0:
2549 self.__review_part(part = part)
2550
2551 return True
2552
2561
2563 target_doc = manage_documents (
2564 parent = self,
2565 msg = _('\nSelect the document into which to move the selected part !\n')
2566 )
2567 if target_doc is None:
2568 return
2569 if not self.__curr_node_data.reattach(pk_doc = target_doc['pk_doc']):
2570 gmGuiHelpers.gm_show_error (
2571 aMessage = _('Cannot move document part.'),
2572 aTitle = _('Moving document part')
2573 )
2574
2576 delete_it = gmGuiHelpers.gm_show_question (
2577 cancel_button = True,
2578 title = _('Deleting document part'),
2579 question = _(
2580 'Are you sure you want to delete the %s part #%s\n'
2581 '\n'
2582 '%s'
2583 'from the following document\n'
2584 '\n'
2585 ' %s (%s)\n'
2586 '%s'
2587 '\n'
2588 'Really delete ?\n'
2589 '\n'
2590 '(this action cannot be reversed)'
2591 ) % (
2592 gmTools.size2str(self.__curr_node_data['size']),
2593 self.__curr_node_data['seq_idx'],
2594 gmTools.coalesce(self.__curr_node_data['obj_comment'], '', ' "%s"\n\n'),
2595 self.__curr_node_data['l10n_type'],
2596 gmDateTime.pydt_strftime(self.__curr_node_data['date_generated'], format = '%Y-%m-%d', accuracy = gmDateTime.acc_days),
2597 gmTools.coalesce(self.__curr_node_data['doc_comment'], '', ' "%s"\n')
2598 )
2599 )
2600 if not delete_it:
2601 return
2602
2603 gmDocuments.delete_document_part (
2604 part_pk = self.__curr_node_data['pk_obj'],
2605 encounter_pk = gmPerson.gmCurrentPatient().emr.active_encounter['pk_encounter']
2606 )
2607
2609
2610 gmHooks.run_hook_script(hook = 'before_%s_doc_part' % action)
2611
2612 wx.BeginBusyCursor()
2613
2614
2615 found, external_cmd = gmShellAPI.detect_external_binary('gm-%s_doc' % action)
2616 if not found:
2617 found, external_cmd = gmShellAPI.detect_external_binary('gm-%s_doc.bat' % action)
2618 if not found:
2619 _log.error('neither of gm-%s_doc or gm-%s_doc.bat found', action, action)
2620 wx.EndBusyCursor()
2621 gmGuiHelpers.gm_show_error (
2622 _('Cannot %(l10n_action)s document part - %(l10n_action)s command not found.\n'
2623 '\n'
2624 'Either of gm-%(action)s_doc or gm-%(action)s_doc.bat\n'
2625 'must be in the execution path. The command will\n'
2626 'be passed the filename to %(l10n_action)s.'
2627 ) % {'action': action, 'l10n_action': l10n_action},
2628 _('Processing document part: %s') % l10n_action
2629 )
2630 return
2631
2632 cfg = gmCfg.cCfgSQL()
2633
2634
2635 chunksize = int(cfg.get2 (
2636 option = "horstspace.blob_export_chunk_size",
2637 workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace,
2638 bias = 'workplace',
2639 default = default_chunksize
2640 ))
2641
2642 part_file = self.__curr_node_data.save_to_file(aChunkSize = chunksize)
2643
2644 if action == 'print':
2645 cmd = '%s generic_document %s' % (external_cmd, part_file)
2646 else:
2647 cmd = '%s %s' % (external_cmd, part_file)
2648 if os.name == 'nt':
2649 blocking = True
2650 else:
2651 blocking = False
2652 success = gmShellAPI.run_command_in_shell (
2653 command = cmd,
2654 blocking = blocking
2655 )
2656
2657 wx.EndBusyCursor()
2658
2659 if not success:
2660 _log.error('%s command failed: [%s]', action, cmd)
2661 gmGuiHelpers.gm_show_error (
2662 _('Cannot %(l10n_action)s document part - %(l10n_action)s command failed.\n'
2663 '\n'
2664 'You may need to check and fix either of\n'
2665 ' gm-%(action)s_doc (Unix/Mac) or\n'
2666 ' gm-%(action)s_doc.bat (Windows)\n'
2667 '\n'
2668 'The command is passed the filename to %(l10n_action)s.'
2669 ) % {'action': action, 'l10n_action': l10n_action},
2670 _('Processing document part: %s') % l10n_action
2671 )
2672 else:
2673 if action == 'mail':
2674 curr_pat = gmPerson.gmCurrentPatient()
2675 emr = curr_pat.emr
2676 emr.add_clin_narrative (
2677 soap_cat = None,
2678 note = _('document part handed over to email program: %s') % self.__curr_node_data.format(single_line = True),
2679 episode = self.__curr_node_data['pk_episode']
2680 )
2681
2683 self.__process_part(action = 'print', l10n_action = _('print'))
2684
2686 self.__process_part(action = 'fax', l10n_action = _('fax'))
2687
2689 self.__process_part(action = 'mail', l10n_action = _('mail'))
2690
2692 """Save document part into directory."""
2693
2694 dlg = wx.DirDialog (
2695 parent = self,
2696 message = _('Save document part to directory ...'),
2697 defaultPath = os.path.expanduser(os.path.join('~', 'gnumed')),
2698 style = wx.DD_DEFAULT_STYLE
2699 )
2700 result = dlg.ShowModal()
2701 dirname = dlg.GetPath()
2702 dlg.Destroy()
2703
2704 if result != wx.ID_OK:
2705 return True
2706
2707 wx.BeginBusyCursor()
2708
2709 pat = gmPerson.gmCurrentPatient()
2710 fname = self.__curr_node_data.get_useful_filename (
2711 patient = pat,
2712 make_unique = True,
2713 directory = dirname
2714 )
2715
2716 cfg = gmCfg.cCfgSQL()
2717
2718
2719 chunksize = int(cfg.get2 (
2720 option = "horstspace.blob_export_chunk_size",
2721 workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace,
2722 bias = 'workplace',
2723 default = default_chunksize
2724 ))
2725
2726 fname = self.__curr_node_data.save_to_file (
2727 aChunkSize = chunksize,
2728 filename = fname,
2729 target_mime = None
2730 )
2731
2732 wx.EndBusyCursor()
2733
2734 gmDispatcher.send(signal = 'statustext', msg = _('Successfully saved document part as [%s].') % fname)
2735
2736 return True
2737
2738
2739
2740
2750
2754
2756
2757 gmHooks.run_hook_script(hook = 'before_%s_doc' % action)
2758
2759 wx.BeginBusyCursor()
2760
2761
2762 found, external_cmd = gmShellAPI.detect_external_binary('gm-%s_doc' % action)
2763 if not found:
2764 found, external_cmd = gmShellAPI.detect_external_binary('gm-%s_doc.bat' % action)
2765 if not found:
2766 _log.error('neither of gm-%s_doc or gm-%s_doc.bat found', action, action)
2767 wx.EndBusyCursor()
2768 gmGuiHelpers.gm_show_error (
2769 _('Cannot %(l10n_action)s document - %(l10n_action)s command not found.\n'
2770 '\n'
2771 'Either of gm-%(action)s_doc or gm-%(action)s_doc.bat\n'
2772 'must be in the execution path. The command will\n'
2773 'be passed a list of filenames to %(l10n_action)s.'
2774 ) % {'action': action, 'l10n_action': l10n_action},
2775 _('Processing document: %s') % l10n_action
2776 )
2777 return
2778
2779 cfg = gmCfg.cCfgSQL()
2780
2781
2782 chunksize = int(cfg.get2 (
2783 option = "horstspace.blob_export_chunk_size",
2784 workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace,
2785 bias = 'workplace',
2786 default = default_chunksize
2787 ))
2788
2789 part_files = self.__curr_node_data.save_parts_to_files(chunksize = chunksize)
2790
2791 if os.name == 'nt':
2792 blocking = True
2793 else:
2794 blocking = False
2795
2796 if action == 'print':
2797 cmd = '%s %s %s' % (
2798 external_cmd,
2799 'generic_document',
2800 ' '.join(part_files)
2801 )
2802 else:
2803 cmd = external_cmd + ' ' + ' '.join(part_files)
2804 success = gmShellAPI.run_command_in_shell (
2805 command = cmd,
2806 blocking = blocking
2807 )
2808
2809 wx.EndBusyCursor()
2810
2811 if not success:
2812 _log.error('%s command failed: [%s]', action, cmd)
2813 gmGuiHelpers.gm_show_error (
2814 _('Cannot %(l10n_action)s document - %(l10n_action)s command failed.\n'
2815 '\n'
2816 'You may need to check and fix either of\n'
2817 ' gm-%(action)s_doc (Unix/Mac) or\n'
2818 ' gm-%(action)s_doc.bat (Windows)\n'
2819 '\n'
2820 'The command is passed a list of filenames to %(l10n_action)s.'
2821 ) % {'action': action, 'l10n_action': l10n_action},
2822 _('Processing document: %s') % l10n_action
2823 )
2824
2825
2827 self.__process_doc(action = 'print', l10n_action = _('print'))
2828
2829
2831 self.__process_doc(action = 'fax', l10n_action = _('fax'))
2832
2833
2835 self.__process_doc(action = 'mail', l10n_action = _('mail'))
2836
2837
2839 dlg = wx.FileDialog (
2840 parent = self,
2841 message = _('Choose a file'),
2842 defaultDir = os.path.expanduser(os.path.join('~', 'gnumed')),
2843 defaultFile = '',
2844 wildcard = "%s (*)|*|PNGs (*.png)|*.png|PDFs (*.pdf)|*.pdf|TIFFs (*.tif)|*.tif|JPEGs (*.jpg)|*.jpg|%s (*.*)|*.*" % (_('all files'), _('all files (Win)')),
2845 style = wx.FD_OPEN | wx.FD_FILE_MUST_EXIST | wx.FD_MULTIPLE
2846 )
2847 result = dlg.ShowModal()
2848 if result != wx.ID_CANCEL:
2849 self.__curr_node_data.add_parts_from_files(files = dlg.GetPaths(), reviewer = gmStaff.gmCurrentProvider()['pk_staff'])
2850 dlg.Destroy()
2851
2852
2867
2869
2870 gmHooks.run_hook_script(hook = 'before_external_doc_access')
2871
2872 wx.BeginBusyCursor()
2873
2874
2875 found, external_cmd = gmShellAPI.detect_external_binary('gm_access_external_doc.sh')
2876 if not found:
2877 found, external_cmd = gmShellAPI.detect_external_binary('gm_access_external_doc.bat')
2878 if not found:
2879 _log.error('neither of gm_access_external_doc.sh or .bat found')
2880 wx.EndBusyCursor()
2881 gmGuiHelpers.gm_show_error (
2882 _('Cannot access external document - access command not found.\n'
2883 '\n'
2884 'Either of gm_access_external_doc.sh or *.bat must be\n'
2885 'in the execution path. The command will be passed the\n'
2886 'document type and the reference URL for processing.'
2887 ),
2888 _('Accessing external document')
2889 )
2890 return
2891
2892 cmd = '%s "%s" "%s"' % (external_cmd, self.__curr_node_data['type'], self.__curr_node_data['ext_ref'])
2893 if os.name == 'nt':
2894 blocking = True
2895 else:
2896 blocking = False
2897 success = gmShellAPI.run_command_in_shell (
2898 command = cmd,
2899 blocking = blocking
2900 )
2901
2902 wx.EndBusyCursor()
2903
2904 if not success:
2905 _log.error('External access command failed: [%s]', cmd)
2906 gmGuiHelpers.gm_show_error (
2907 _('Cannot access external document - access command failed.\n'
2908 '\n'
2909 'You may need to check and fix either of\n'
2910 ' gm_access_external_doc.sh (Unix/Mac) or\n'
2911 ' gm_access_external_doc.bat (Windows)\n'
2912 '\n'
2913 'The command is passed the document type and the\n'
2914 'external reference URL on the command line.'
2915 ),
2916 _('Accessing external document')
2917 )
2918
2920 """Save document into directory.
2921
2922 - one file per object
2923 - into subdirectory named after patient
2924 """
2925 pat = gmPerson.gmCurrentPatient()
2926 def_dir = os.path.expanduser(os.path.join('~', 'gnumed', pat.subdir_name))
2927 gmTools.mkdir(def_dir)
2928
2929 dlg = wx.DirDialog (
2930 parent = self,
2931 message = _('Save document into directory ...'),
2932 defaultPath = def_dir,
2933 style = wx.DD_DEFAULT_STYLE
2934 )
2935 result = dlg.ShowModal()
2936 dirname = dlg.GetPath()
2937 dlg.Destroy()
2938
2939 if result != wx.ID_OK:
2940 return True
2941
2942 wx.BeginBusyCursor()
2943
2944 cfg = gmCfg.cCfgSQL()
2945
2946
2947 chunksize = int(cfg.get2 (
2948 option = "horstspace.blob_export_chunk_size",
2949 workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace,
2950 bias = 'workplace',
2951 default = default_chunksize
2952 ))
2953
2954 fnames = self.__curr_node_data.save_parts_to_files(export_dir = dirname, chunksize = chunksize)
2955
2956 wx.EndBusyCursor()
2957
2958 gmDispatcher.send(signal='statustext', msg=_('Successfully saved %s parts into the directory [%s].') % (len(fnames), dirname))
2959
2960 return True
2961
2962
2965
2966
2977
2978
2979
2980
2981
2982 from Gnumed.wxGladeWidgets.wxgPACSPluginPnl import wxgPACSPluginPnl
2983
2984 -class cPACSPluginPnl(wxgPACSPluginPnl, gmRegetMixin.cRegetOnPaintMixin):
2985
2996
2997
2998
2999
3001
3002 login = gmPG2.get_default_login()
3003 self._TCTRL_host.Value = gmTools.coalesce(login.host, 'localhost')
3004 self._TCTRL_port.Value = '8042'
3005
3006 self._LCTRL_studies.set_columns(columns = [_('Date'), _('Description'), _('Organization'), _('Authority')])
3007 self._LCTRL_studies.select_callback = self._on_studies_list_item_selected
3008 self._LCTRL_studies.deselect_callback = self._on_studies_list_item_deselected
3009
3010 self._LCTRL_series.set_columns(columns = [_('Time'), _('Method'), _('Body part'), _('Description')])
3011 self._LCTRL_series.select_callback = self._on_series_list_item_selected
3012 self._LCTRL_series.deselect_callback = self._on_series_list_item_deselected
3013
3014 self._LCTRL_details.set_columns(columns = [_('DICOM field'), _('Value')])
3015 self._LCTRL_details.set_column_widths()
3016
3017 self._BMP_preview.SetBitmap(wx.Bitmap.FromRGBA(50,50, red=0, green=0, blue=0, alpha = wx.ALPHA_TRANSPARENT))
3018
3019
3086
3087
3089 self._LBL_patient_identification.SetLabel('')
3090 self._LCTRL_studies.set_string_items(items = [])
3091 self._LCTRL_series.set_string_items(items = [])
3092 self.__refresh_image()
3093 self.__refresh_details()
3094
3095
3097 self._LBL_PACS_identification.SetLabel(_('<not connected>'))
3098
3099
3101 self.__reset_server_identification()
3102 self.__reset_patient_data()
3103 self.__set_button_states()
3104
3105
3107
3108 self.__pacs = None
3109 self.__orthanc_patient = None
3110 self.__set_button_states()
3111 self.__reset_server_identification()
3112
3113 host = self._TCTRL_host.Value.strip()
3114 port = self._TCTRL_port.Value.strip()[:6]
3115 if port == '':
3116 self._LBL_PACS_identification.SetLabel(_('Cannot connect without port (try 8042).'))
3117 return False
3118 if len(port) < 4:
3119 return False
3120 try:
3121 int(port)
3122 except ValueError:
3123 self._LBL_PACS_identification.SetLabel(_('Invalid port (try 8042).'))
3124 return False
3125
3126 user = self._TCTRL_user.Value
3127 if user == '':
3128 user = None
3129 self._LBL_PACS_identification.SetLabel(_('Connect to [%s] @ port %s as "%s".') % (host, port, user))
3130 password = self._TCTRL_password.Value
3131 if password == '':
3132 password = None
3133
3134 pacs = gmDICOM.cOrthancServer()
3135 if not pacs.connect(host = host, port = port, user = user, password = password):
3136 self._LBL_PACS_identification.SetLabel(_('Cannot connect to PACS.'))
3137 _log.error('error connecting to server: %s', pacs.connect_error)
3138 return False
3139
3140
3141 self._LBL_PACS_identification.SetLabel(_('PACS: Orthanc "%s" (AET "%s", Version %s, DB v%s)') % (
3142 pacs.server_identification['Name'],
3143 pacs.server_identification['DicomAet'],
3144 pacs.server_identification['Version'],
3145
3146 pacs.server_identification['DatabaseVersion']
3147 ))
3148
3149 self.__pacs = pacs
3150 self.__set_button_states()
3151 return True
3152
3153
3155
3156 self.__orthanc_patient = None
3157
3158 if not self.__patient.connected:
3159 self.__reset_patient_data()
3160 self.__set_button_states()
3161 return True
3162
3163 if not self.__connect():
3164 return False
3165
3166 tt_lines = [_('Known PACS IDs:')]
3167 for pacs_id in self.__patient.suggest_external_ids(target = 'PACS'):
3168 tt_lines.append(' ' + _('generic: %s') % pacs_id)
3169 for pacs_id in self.__patient.get_external_ids(id_type = 'PACS', issuer = self.__pacs.as_external_id_issuer):
3170 tt_lines.append(' ' + _('stored: "%(value)s" @ [%(issuer)s]') % pacs_id)
3171 tt_lines.append('')
3172 tt_lines.append(_('Patients found in PACS:'))
3173
3174 info_lines = []
3175
3176 matching_pats = self.__pacs.get_matching_patients(person = self.__patient)
3177 if len(matching_pats) == 0:
3178 info_lines.append(_('PACS: no patients with matching IDs found'))
3179 no_of_studies = 0
3180 for pat in matching_pats:
3181 info_lines.append('"%s" %s "%s (%s) %s"' % (
3182 pat['MainDicomTags']['PatientID'],
3183 gmTools.u_arrow2right,
3184 gmTools.coalesce(pat['MainDicomTags']['PatientName'], '?'),
3185 gmTools.coalesce(pat['MainDicomTags']['PatientSex'], '?'),
3186 gmTools.coalesce(pat['MainDicomTags']['PatientBirthDate'], '?')
3187 ))
3188 no_of_studies += len(pat['Studies'])
3189 tt_lines.append('%s [#%s]' % (
3190 gmTools.format_dict_like (
3191 pat['MainDicomTags'],
3192 relevant_keys = ['PatientName', 'PatientSex', 'PatientBirthDate', 'PatientID'],
3193 template = ' %(PatientID)s = %(PatientName)s (%(PatientSex)s) %(PatientBirthDate)s',
3194 missing_key_template = '?'
3195 ),
3196 pat['ID']
3197 ))
3198 if len(matching_pats) > 1:
3199 info_lines.append(_('PACS: more than one patient with matching IDs found, carefully check studies'))
3200 self._LBL_patient_identification.SetLabel('\n'.join(info_lines))
3201 tt_lines.append('')
3202 tt_lines.append(_('Studies found: %s') % no_of_studies)
3203 self._LBL_patient_identification.SetToolTip('\n'.join(tt_lines))
3204
3205
3206 study_list_items = []
3207 study_list_data = []
3208 if len(matching_pats) > 0:
3209
3210 self.__orthanc_patient = matching_pats[0]
3211 for pat in self.__pacs.get_studies_list_by_orthanc_patient_list(orthanc_patients = matching_pats):
3212 for study in pat['studies']:
3213 docs = []
3214 if study['referring_doc'] is not None:
3215 docs.append(study['referring_doc'])
3216 if study['requesting_doc'] is not None:
3217 if study['requesting_doc'] not in docs:
3218 docs.append(study['requesting_doc'])
3219 if study['performing_doc'] is not None:
3220 if study['performing_doc'] not in docs:
3221 docs.append(study['requesting_doc'])
3222 if study['operator_name'] is not None:
3223 if study['operator_name'] not in docs:
3224 docs.append(study['operator_name'])
3225 if study['radiographer_code'] is not None:
3226 if study['radiographer_code'] not in docs:
3227 docs.append(study['radiographer_code'])
3228 org_name = u'@'.join ([
3229 o for o in [study['radiology_dept'], study['radiology_org']]
3230 if o is not None
3231 ])
3232 org = '%s%s%s' % (
3233 org_name,
3234 gmTools.coalesce(study['station_name'], '', ' [%s]'),
3235 gmTools.coalesce(study['radiology_org_addr'], '', ' (%s)').replace('\r\n', ' [CR] ')
3236 )
3237 study_list_items.append( [
3238 '%s-%s-%s' % (
3239 study['date'][:4],
3240 study['date'][4:6],
3241 study['date'][6:8]
3242 ),
3243 _('%s series%s') % (
3244 len(study['series']),
3245 gmTools.coalesce(study['description'], '', ': %s')
3246 ),
3247 org.strip(),
3248 gmTools.u_arrow2right.join(docs)
3249 ] )
3250 study_list_data.append(study)
3251
3252 self._LCTRL_studies.set_string_items(items = study_list_items)
3253 self._LCTRL_studies.set_data(data = study_list_data)
3254 self._LCTRL_studies.SortListItems(0, 0)
3255 self._LCTRL_studies.set_column_widths()
3256
3257 self.__refresh_image()
3258 self.__refresh_details()
3259 self.__set_button_states()
3260
3261 return True
3262
3263
3297
3298
3300
3301 self.__image_data = None
3302 self._LBL_image.Label = _('Image')
3303 self._BMP_preview.SetBitmap(wx.Bitmap.FromRGBA(50,50, red=0, green=0, blue=0, alpha = wx.ALPHA_TRANSPARENT))
3304
3305 if idx is None:
3306 self._BMP_preview.ContainingSizer.Layout()
3307 return
3308 if self.__pacs is None:
3309 self._BMP_preview.ContainingSizer.Layout()
3310 return
3311 series = self._LCTRL_series.get_selected_item_data(only_one = True)
3312 if series is None:
3313 self._BMP_preview.ContainingSizer.Layout()
3314 return
3315 if idx > len(series['instances']) - 1:
3316 raise ValueError('trying to go beyond instances in series: %s of %s', idx, len(series['instances']))
3317
3318
3319 uuid = series['instances'][idx]
3320 img_file = self.__pacs.get_instance_preview(instance_id = uuid)
3321
3322 wx_bmp = gmGuiHelpers.file2scaled_image(filename = img_file, height = 100)
3323
3324 if wx_bmp is None:
3325 _log.error('cannot load DICOM instance from PACS: %s', uuid)
3326 else:
3327 self.__image_data = {'idx': idx, 'uuid': uuid}
3328 self._BMP_preview.SetBitmap(wx_bmp)
3329 self._LBL_image.Label = _('Image %s/%s') % (idx+1, len(series['instances']))
3330
3331 if idx == 0:
3332 self._BTN_previous_image.Disable()
3333 else:
3334 self._BTN_previous_image.Enable()
3335 if idx == len(series['instances']) - 1:
3336 self._BTN_next_image.Disable()
3337 else:
3338 self._BTN_next_image.Enable()
3339
3340 self._BMP_preview.ContainingSizer.Layout()
3341
3342
3343
3344
3346 if not self.__patient.connected:
3347 self.__reset_ui_content()
3348 return True
3349
3350 if not self.__refresh_patient_data():
3351 return False
3352
3353 return True
3354
3355
3356
3357
3359
3360 gmDispatcher.connect(signal = 'pre_patient_unselection', receiver = self._on_pre_patient_unselection)
3361 gmDispatcher.connect(signal = 'post_patient_selection', receiver = self._on_post_patient_selection)
3362
3363
3364 gmDispatcher.connect(signal = 'gm_table_mod', receiver = self._on_database_signal)
3365
3366
3368
3369
3370
3371 self.__reset_patient_data()
3372
3373
3375 self._schedule_data_reget()
3376
3377
3379
3380 if not self.__patient.connected:
3381
3382
3383 return True
3384
3385 if kwds['pk_identity'] != self.__patient.ID:
3386 return True
3387
3388 if kwds['table'] == 'dem.lnk_identity2ext_id':
3389 self._schedule_data_reget()
3390 return True
3391
3392 return True
3393
3394
3395
3396
3398
3399 event.Skip()
3400 if self.__pacs is None:
3401 return
3402
3403 study_data = self._LCTRL_studies.get_selected_item_data(only_one = True)
3404 if study_data is None:
3405 return
3406
3407 series = self._LCTRL_series.get_selected_item_data(only_one = True)
3408 if series is None:
3409 self.__set_button_states()
3410 return
3411
3412 if len(series['instances']) == 0:
3413 self.__refresh_image()
3414 self.__refresh_details()
3415 self.__set_button_states()
3416 return
3417
3418
3419 self.__refresh_image(0)
3420 self.__refresh_details()
3421 self.__set_button_states()
3422 self._BTN_previous_image.Disable()
3423
3424
3426 event.Skip()
3427
3428 self.__refresh_image()
3429 self.__refresh_details()
3430 self.__set_button_states()
3431
3432
3434 event.Skip()
3435 if self.__pacs is None:
3436 return
3437
3438 study_data = self._LCTRL_studies.get_selected_item_data(only_one = True)
3439 if study_data is None:
3440 self.__set_button_states()
3441 return
3442
3443 series_list_items = []
3444 series_list_data = []
3445 for series in study_data['series']:
3446
3447 series_time = ''
3448 if series['time'] is None:
3449 series['time'] = study_data['time']
3450 series_time = '%s:%s:%s' % (
3451 series['time'][:2],
3452 series['time'][2:4],
3453 series['time'][4:6]
3454 )
3455
3456 series_desc_parts = []
3457 if series['description'] is not None:
3458 if series['protocol'] is None:
3459 series_desc_parts.append(series['description'].strip())
3460 else:
3461 if series['description'].strip() not in series['protocol'].strip():
3462 series_desc_parts.append(series['description'].strip())
3463 if series['protocol'] is not None:
3464 series_desc_parts.append('[%s]' % series['protocol'].strip())
3465 if series['performed_procedure_step_description'] is not None:
3466 series_desc_parts.append(series['performed_procedure_step_description'].strip())
3467 if series['acquisition_device_processing_description'] is not None:
3468 series_desc_parts.append(series['acquisition_device_processing_description'].strip())
3469 series_desc = ' / '.join(series_desc_parts)
3470 if len(series_desc) > 0:
3471 series_desc = ': ' + series_desc
3472 series_desc = _('%s image(s)%s') % (len(series['instances']), series_desc)
3473
3474 series_list_items.append ([
3475 series_time,
3476 gmTools.coalesce(series['modality'], ''),
3477 gmTools.coalesce(series['body_part'], ''),
3478 series_desc
3479 ])
3480 series_list_data.append(series)
3481
3482 self._LCTRL_series.set_string_items(items = series_list_items)
3483 self._LCTRL_series.set_data(data = series_list_data)
3484 self._LCTRL_series.SortListItems(0)
3485
3486 self.__refresh_image()
3487 self.__refresh_details()
3488 self.__set_button_states()
3489
3490
3492 event.Skip()
3493
3494 self._LCTRL_series.remove_items_safely()
3495 self.__refresh_image()
3496 self.__refresh_details()
3497 self.__set_button_states()
3498
3499
3500
3501
3516
3517
3523
3524
3530
3531
3540
3541
3586
3587
3589 event.Skip()
3590 if self.__pacs is None:
3591 return
3592
3593 title = _('Working on: Orthanc "%s" (AET "%s" @ %s:%s, Version %s)') % (
3594 self.__pacs.server_identification['Name'],
3595 self.__pacs.server_identification['DicomAet'],
3596 self._TCTRL_host.Value.strip(),
3597 self._TCTRL_port.Value.strip(),
3598 self.__pacs.server_identification['Version']
3599 )
3600 dlg = cModifyOrthancContentDlg(self, -1, server = self.__pacs, title = title)
3601 dlg.ShowModal()
3602 dlg.Destroy()
3603 self._schedule_data_reget()
3604
3605
3606
3607
3613
3614
3620
3621
3635
3636
3649
3650
3663
3664
3677
3678
3700
3701
3702
3703
3737
3738
3764
3765
3766
3767
3811
3812
3844
3845
3870
3871
3872
3873
3874
3875
3876
3877
3878
3879
3880
3881
3882
3883
3884
3885
3886
3887
3888
3889
3890
3891
3892
3893
3894
3895
3896
3897
3898
3899
3900
3901
3902
3903
3904
3905
3906
3907
3908
3909
3910
3911
3912
3913
3914
3915
3916
3917
3918
3919
3920
3921
3922
3923
3924
3925
3926
3927
3928
3929
3930
3931
3932
3933
3934
3935
3936
3937
3938
3939
3940
3941
3942
3943
3944
3945
3946
3947 from Gnumed.wxGladeWidgets.wxgModifyOrthancContentDlg import wxgModifyOrthancContentDlg
3948
3949 -class cModifyOrthancContentDlg(wxgModifyOrthancContentDlg):
3950 - def __init__(self, *args, **kwds):
3951 self.__srv = kwds['server']
3952 del kwds['server']
3953 title = kwds['title']
3954 del kwds['title']
3955 wxgModifyOrthancContentDlg.__init__(self, *args, **kwds)
3956 self.SetTitle(title)
3957 self._LCTRL_patients.set_columns( [_('Patient ID'), _('Name'), _('Birth date'), _('Gender'), _('Orthanc')] )
3958
3959
3961 self._LCTRL_patients.set_string_items()
3962 search_term = self._TCTRL_search_term.Value.strip()
3963 if search_term == '':
3964 return
3965 pats = self.__srv.get_patients_by_name(name_parts = search_term.split(), fuzzy = True)
3966 if len(pats) == 0:
3967 return
3968 list_items = []
3969 list_data = []
3970 for pat in pats:
3971 mt = pat['MainDicomTags']
3972 try:
3973 gender = mt['PatientSex']
3974 except KeyError:
3975 gender = ''
3976 try:
3977 dob = mt['PatientBirthDate']
3978 except KeyError:
3979 dob = ''
3980 list_items.append([mt['PatientID'], mt['PatientName'], dob, gender, pat['ID']])
3981 list_data.append(mt['PatientID'])
3982 self._LCTRL_patients.set_string_items(list_items)
3983 self._LCTRL_patients.set_column_widths()
3984 self._LCTRL_patients.set_data(list_data)
3985
3986
3988 event.Skip()
3989 self.__refresh_patient_list()
3990
3991
3998
3999
4001 event.Skip()
4002 new_id = self._TCTRL_new_patient_id.Value.strip()
4003 if new_id == '':
4004 return
4005 pats = self._LCTRL_patients.get_selected_item_data(only_one = False)
4006 if len(pats) == 0:
4007 return
4008 really_modify = gmGuiHelpers.gm_show_question (
4009 title = _('Modifying patient ID'),
4010 question = _(
4011 'Really modify %s patient(s) to have the new patient ID\n\n'
4012 ' [%s]\n\n'
4013 'stored in the Orthanc DICOM server ?'
4014 ) % (
4015 len(pats),
4016 new_id
4017 ),
4018 cancel_button = True
4019 )
4020 if not really_modify:
4021 return
4022 all_modified = True
4023 for pat in pats:
4024 success = self.__srv.modify_patient_id(old_patient_id = pat, new_patient_id = new_id)
4025 if not success:
4026 all_modified = False
4027 self.__refresh_patient_list()
4028
4029 if not all_modified:
4030 gmGuiHelpers.gm_show_warning (
4031 aTitle = _('Modifying patient ID'),
4032 aMessage = _(
4033 'I was unable to modify all DICOM patients.\n'
4034 '\n'
4035 'Please refer to the log file.'
4036 )
4037 )
4038 return all_modified
4039
4040
4041
4043 event.Skip()
4044 dlg = wx.DirDialog (
4045 self,
4046 message = _('Select the directory from which to recursively upload DICOM files.'),
4047 defaultPath = os.path.join(gmTools.gmPaths().home_dir, 'gnumed')
4048 )
4049 choice = dlg.ShowModal()
4050 dicom_dir = dlg.GetPath()
4051 dlg.Destroy()
4052 if choice != wx.ID_OK:
4053 return True
4054 wx.BeginBusyCursor()
4055 try:
4056 uploaded, not_uploaded = self.__pacs.upload_from_directory (
4057 directory = dicom_dir,
4058 recursive = True,
4059 check_mime_type = False,
4060 ignore_other_files = True
4061 )
4062 finally:
4063 wx.EndBusyCursor()
4064 if len(not_uploaded) == 0:
4065 q = _('Delete the uploaded DICOM files now ?')
4066 else:
4067 q = _('Some files have not been uploaded.\n\nDo you want to delete those DICOM files which have been sent to the PACS successfully ?')
4068 _log.error('not uploaded:')
4069 for f in not_uploaded:
4070 _log.error(f)
4071 delete_uploaded = gmGuiHelpers.gm_show_question (
4072 title = _('Uploading DICOM files'),
4073 question = q,
4074 cancel_button = False
4075 )
4076 if not delete_uploaded:
4077 return
4078 wx.BeginBusyCursor()
4079 for f in uploaded:
4080 gmTools.remove_file(f)
4081 wx.EndBusyCursor()
4082
4083
4084
4085
4086 if __name__ == '__main__':
4087
4088 if len(sys.argv) < 2:
4089 sys.exit()
4090
4091 if sys.argv[1] != 'test':
4092 sys.exit()
4093
4094 from Gnumed.business import gmPersonSearch
4095 from Gnumed.wxpython import gmPatSearchWidgets
4096
4097
4099 app = wx.PyWidgetTester(size = (180, 20))
4100
4101 prw = cDocumentPhraseWheel(app.frame, -1)
4102 prw.set_context('pat', 12)
4103 app.frame.Show(True)
4104 app.MainLoop()
4105
4106
4107 test_document_prw()
4108