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
48 from Gnumed.wxpython import gmGuiHelpers
49 from Gnumed.wxpython import gmRegetMixin
50 from Gnumed.wxpython import gmPhraseWheel
51 from Gnumed.wxpython import gmPlugin
52 from Gnumed.wxpython import gmEncounterWidgets
53 from Gnumed.wxpython import gmListWidgets
54 from Gnumed.wxpython import gmRegetMixin
55
56
57 _log = logging.getLogger('gm.ui')
58
59
60 default_chunksize = 1 * 1024 * 1024
61
62
64
65
66 def delete_item(item):
67 doit = gmGuiHelpers.gm_show_question (
68 _( 'Are you sure you want to delete this\n'
69 'description from the document ?\n'
70 ),
71 _('Deleting document description')
72 )
73 if not doit:
74 return True
75
76 document.delete_description(pk = item[0])
77 return True
78
79 def add_item():
80 dlg = gmGuiHelpers.cMultilineTextEntryDlg (
81 parent,
82 -1,
83 title = _('Adding document description'),
84 msg = _('Below you can add a document description.\n')
85 )
86 result = dlg.ShowModal()
87 if result == wx.ID_SAVE:
88 document.add_description(dlg.value)
89
90 dlg.Destroy()
91 return True
92
93 def edit_item(item):
94 dlg = gmGuiHelpers.cMultilineTextEntryDlg (
95 parent,
96 -1,
97 title = _('Editing document description'),
98 msg = _('Below you can edit the document description.\n'),
99 text = item[1]
100 )
101 result = dlg.ShowModal()
102 if result == wx.ID_SAVE:
103 document.update_description(pk = item[0], description = dlg.value)
104
105 dlg.Destroy()
106 return True
107
108 def refresh_list(lctrl):
109 descriptions = document.get_descriptions()
110
111 lctrl.set_string_items(items = [
112 '%s%s' % ( (' '.join(regex.split('\r\n+|\r+|\n+|\t+', desc[1])))[:30], gmTools.u_ellipsis )
113 for desc in descriptions
114 ])
115 lctrl.set_data(data = descriptions)
116
117
118 gmListWidgets.get_choices_from_list (
119 parent = parent,
120 msg = _('Select the description you are interested in.\n'),
121 caption = _('Managing document descriptions'),
122 columns = [_('Description')],
123 edit_callback = edit_item,
124 new_callback = add_item,
125 delete_callback = delete_item,
126 refresh_callback = refresh_list,
127 single_selection = True,
128 can_return_empty = True
129 )
130
131 return True
132
133
135 try:
136 del kwargs['signal']
137 del kwargs['sender']
138 except KeyError:
139 pass
140 wx.CallAfter(save_file_as_new_document, **kwargs)
141
143 try:
144 del kwargs['signal']
145 del kwargs['sender']
146 except KeyError:
147 pass
148 wx.CallAfter(save_files_as_new_document, **kwargs)
149
150 -def save_file_as_new_document(parent=None, filename=None, document_type=None, unlock_patient=False, episode=None, review_as_normal=False, pk_org_unit=None):
160
161
162 -def save_files_as_new_document(parent=None, filenames=None, document_type=None, unlock_patient=False, episode=None, review_as_normal=False, reference=None, pk_org_unit=None, date_generated=None, comment=None, reviewer=None, pk_document_type=None):
163
164 pat = gmPerson.gmCurrentPatient()
165 if not pat.connected:
166 return None
167
168 emr = pat.emr
169
170 if parent is None:
171 parent = wx.GetApp().GetTopWindow()
172
173 if episode is None:
174 all_epis = emr.get_episodes()
175
176 if len(all_epis) == 0:
177 episode = emr.add_episode(episode_name = _('Documents'), is_open = False)
178 else:
179 from Gnumed.wxpython.gmEMRStructWidgets import cEpisodeListSelectorDlg
180 dlg = cEpisodeListSelectorDlg(parent, -1, episodes = all_epis)
181 dlg.SetTitle(_('Select the episode under which to file the document ...'))
182 btn_pressed = dlg.ShowModal()
183 episode = dlg.get_selected_item_data(only_one = True)
184 dlg.Destroy()
185
186 if (btn_pressed == wx.ID_CANCEL) or (episode is None):
187 if unlock_patient:
188 pat.locked = False
189 return None
190
191 wx.BeginBusyCursor()
192
193 if pk_document_type is None:
194 pk_document_type = gmDocuments.create_document_type(document_type = document_type)['pk_doc_type']
195
196 docs_folder = pat.get_document_folder()
197 doc = docs_folder.add_document (
198 document_type = pk_document_type,
199 encounter = emr.active_encounter['pk_encounter'],
200 episode = episode['pk_episode']
201 )
202 if doc is None:
203 wx.EndBusyCursor()
204 gmGuiHelpers.gm_show_error (
205 aMessage = _('Cannot create new document.'),
206 aTitle = _('saving document')
207 )
208 return False
209
210 if reference is not None:
211 doc['ext_ref'] = reference
212 if pk_org_unit is not None:
213 doc['pk_org_unit'] = pk_org_unit
214 if date_generated is not None:
215 doc['clin_when'] = date_generated
216 if comment is not None:
217 if comment != '':
218 doc['comment'] = comment
219 doc.save()
220
221 success, msg, filename = doc.add_parts_from_files(files = filenames, reviewer = reviewer)
222 if not success:
223 wx.EndBusyCursor()
224 gmGuiHelpers.gm_show_error (
225 aMessage = msg,
226 aTitle = _('saving document')
227 )
228 return False
229
230 if review_as_normal:
231 doc.set_reviewed(technically_abnormal = False, clinically_relevant = False)
232
233 if unlock_patient:
234 pat.locked = False
235
236 gmDispatcher.send(signal = 'statustext', msg = _('Imported new document from %s.') % filenames, beep = True)
237
238
239 cfg = gmCfg.cCfgSQL()
240 show_id = bool (
241 cfg.get2 (
242 option = 'horstspace.scan_index.show_doc_id',
243 workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace,
244 bias = 'user'
245 )
246 )
247
248 wx.EndBusyCursor()
249
250 if not show_id:
251 gmDispatcher.send(signal = 'statustext', msg = _('Successfully saved new document.'))
252 else:
253 if reference is None:
254 msg = _('Successfully saved the new document.')
255 else:
256 msg = _('The reference ID for the new document is:\n'
257 '\n'
258 ' <%s>\n'
259 '\n'
260 'You probably want to write it down on the\n'
261 'original documents.\n'
262 '\n'
263 "If you don't care about the ID you can switch\n"
264 'off this message in the GNUmed configuration.\n'
265 ) % reference
266 gmGuiHelpers.gm_show_info (
267 aMessage = msg,
268 aTitle = _('Saving document')
269 )
270
271
272 tmp_dir = gmTools.gmPaths().tmp_dir
273 files2remove = [ f for f in filenames if not f.startswith(tmp_dir) ]
274 if len(files2remove) > 0:
275 do_delete = gmGuiHelpers.gm_show_question (
276 _( 'Successfully imported files as document.\n'
277 '\n'
278 'Do you want to delete imported files from the filesystem ?\n'
279 '\n'
280 ' %s'
281 ) % '\n '.join(files2remove),
282 _('Removing files')
283 )
284 if do_delete:
285 for fname in files2remove:
286 gmTools.remove_file(fname)
287
288 return doc
289
290
291 gmDispatcher.connect(signal = 'import_document_from_file', receiver = _save_file_as_new_document)
292 gmDispatcher.connect(signal = 'import_document_from_files', receiver = _save_files_as_new_document)
293
294
296
298
299 gmPhraseWheel.cPhraseWheel.__init__(self, *args, **kwargs)
300
301 ctxt = {'ctxt_pat': {
302 'where_part': '(pk_patient = %(pat)s) AND',
303 'placeholder': 'pat'
304 }}
305
306 mp = gmMatchProvider.cMatchProvider_SQL2 (
307 queries = ["""
308 SELECT DISTINCT ON (list_label)
309 pk_doc AS data,
310 l10n_type || ' (' || to_char(clin_when, 'YYYY Mon DD') || ')' || coalesce(': ' || unit || '@' || organization, '') || ' - ' || episode || coalesce(' (' || health_issue || ')', '') AS list_label,
311 l10n_type || ' (' || to_char(clin_when, 'YYYY Mon DD') || ')' || coalesce(': ' || organization, '') || ' - ' || coalesce(' (' || health_issue || ')', episode) AS field_label
312 FROM blobs.v_doc_med
313 WHERE
314 %(ctxt_pat)s
315 (
316 l10n_type %(fragment_condition)s
317 OR
318 unit %(fragment_condition)s
319 OR
320 organization %(fragment_condition)s
321 OR
322 episode %(fragment_condition)s
323 OR
324 health_issue %(fragment_condition)s
325 )
326 ORDER BY list_label
327 LIMIT 25"""],
328 context = ctxt
329 )
330 mp.setThresholds(1, 3, 5)
331 mp.unset_context('pat')
332
333 self.matcher = mp
334 self.picklist_delay = 50
335 self.selection_only = True
336
337 self.SetToolTip(_('Select a document.'))
338
339
344
345
350
351
408
409
410
411
419
420
421 from Gnumed.wxGladeWidgets import wxgEditDocumentTypesDlg
422
424 """A dialog showing a cEditDocumentTypesPnl."""
425
428
429
430 from Gnumed.wxGladeWidgets import wxgEditDocumentTypesPnl
431
433 """A panel grouping together fields to edit the list of document types."""
434
440
444
446 gmDispatcher.connect(signal = 'blobs.doc_type_mod_db', receiver = self._on_doc_type_mod_db)
447
450
452
453 self._LCTRL_doc_type.DeleteAllItems()
454
455 doc_types = gmDocuments.get_document_types()
456 pos = len(doc_types) + 1
457
458 for doc_type in doc_types:
459 row_num = self._LCTRL_doc_type.InsertItem(pos, label = doc_type['type'])
460 self._LCTRL_doc_type.SetItem(index = row_num, column = 1, label = doc_type['l10n_type'])
461 if doc_type['is_user_defined']:
462 self._LCTRL_doc_type.SetItem(index = row_num, column = 2, label = ' X ')
463 if doc_type['is_in_use']:
464 self._LCTRL_doc_type.SetItem(index = row_num, column = 3, label = ' X ')
465
466 if len(doc_types) > 0:
467 self._LCTRL_doc_type.set_data(data = doc_types)
468 self._LCTRL_doc_type.SetColumnWidth(0, wx.LIST_AUTOSIZE)
469 self._LCTRL_doc_type.SetColumnWidth(1, wx.LIST_AUTOSIZE)
470 self._LCTRL_doc_type.SetColumnWidth(2, wx.LIST_AUTOSIZE_USEHEADER)
471 self._LCTRL_doc_type.SetColumnWidth(3, wx.LIST_AUTOSIZE_USEHEADER)
472
473 self._TCTRL_type.SetValue('')
474 self._TCTRL_l10n_type.SetValue('')
475
476 self._BTN_set_translation.Enable(False)
477 self._BTN_delete.Enable(False)
478 self._BTN_add.Enable(False)
479 self._BTN_reassign.Enable(False)
480
481 self._LCTRL_doc_type.SetFocus()
482
483
484
486 doc_type = self._LCTRL_doc_type.get_selected_item_data()
487
488 self._TCTRL_type.SetValue(doc_type['type'])
489 self._TCTRL_l10n_type.SetValue(doc_type['l10n_type'])
490
491 self._BTN_set_translation.Enable(True)
492 self._BTN_delete.Enable(not bool(doc_type['is_in_use']))
493 self._BTN_add.Enable(False)
494 self._BTN_reassign.Enable(True)
495
496 return
497
499 self._BTN_set_translation.Enable(False)
500 self._BTN_delete.Enable(False)
501 self._BTN_reassign.Enable(False)
502
503 self._BTN_add.Enable(True)
504
505 return
506
513
530
540
572
573
575 """Let user select a document type."""
577
578 gmPhraseWheel.cPhraseWheel.__init__(self, *args, **kwargs)
579
580 mp = gmMatchProvider.cMatchProvider_SQL2 (
581 queries = [
582 """SELECT
583 data,
584 field_label,
585 list_label
586 FROM ((
587 SELECT
588 pk_doc_type AS data,
589 l10n_type AS field_label,
590 l10n_type AS list_label,
591 1 AS rank
592 FROM blobs.v_doc_type
593 WHERE
594 is_user_defined IS True
595 AND
596 l10n_type %(fragment_condition)s
597 ) UNION (
598 SELECT
599 pk_doc_type AS data,
600 l10n_type AS field_label,
601 l10n_type AS list_label,
602 2 AS rank
603 FROM blobs.v_doc_type
604 WHERE
605 is_user_defined IS False
606 AND
607 l10n_type %(fragment_condition)s
608 )) AS q1
609 ORDER BY q1.rank, q1.list_label"""]
610 )
611 mp.setThresholds(2, 4, 6)
612
613 self.matcher = mp
614 self.picklist_delay = 50
615
616 self.SetToolTip(_('Select the document type.'))
617
619
620 doc_type = self.GetValue().strip()
621 if doc_type == '':
622 gmDispatcher.send(signal = 'statustext', msg = _('Cannot create document type without name.'), beep = True)
623 _log.debug('cannot create document type without name')
624 return
625
626 pk = gmDocuments.create_document_type(doc_type)['pk_doc_type']
627 if pk is None:
628 self.data = {}
629 else:
630 self.SetText (
631 value = doc_type,
632 data = pk
633 )
634
635
636
637
648
649
652
653
654 from Gnumed.wxGladeWidgets import wxgReviewDocPartDlg
655
658 """Support parts and docs now.
659 """
660 part = kwds['part']
661 del kwds['part']
662 wxgReviewDocPartDlg.wxgReviewDocPartDlg.__init__(self, *args, **kwds)
663
664 if isinstance(part, gmDocuments.cDocumentPart):
665 self.__part = part
666 self.__doc = self.__part.get_containing_document()
667 self.__reviewing_doc = False
668 elif isinstance(part, gmDocuments.cDocument):
669 self.__doc = part
670 if len(self.__doc.parts) == 0:
671 self.__part = None
672 else:
673 self.__part = self.__doc.parts[0]
674 self.__reviewing_doc = True
675 else:
676 raise ValueError('<part> must be gmDocuments.cDocument or gmDocuments.cDocumentPart instance, got <%s>' % type(part))
677
678 self.__init_ui_data()
679
680
681
682
684
685
686 self._PhWheel_episode.SetText('%s ' % self.__doc['episode'], self.__doc['pk_episode'])
687 self._PhWheel_doc_type.SetText(value = self.__doc['l10n_type'], data = self.__doc['pk_type'])
688 self._PhWheel_doc_type.add_callback_on_set_focus(self._on_doc_type_gets_focus)
689 self._PhWheel_doc_type.add_callback_on_lose_focus(self._on_doc_type_loses_focus)
690
691 if self.__reviewing_doc:
692 self._PRW_doc_comment.SetText(gmTools.coalesce(self.__doc['comment'], ''))
693 self._PRW_doc_comment.set_context(context = 'pk_doc_type', val = self.__doc['pk_type'])
694 else:
695 self._PRW_doc_comment.SetText(gmTools.coalesce(self.__part['obj_comment'], ''))
696
697 if self.__doc['pk_org_unit'] is not None:
698 self._PRW_org.SetText(value = '%s @ %s' % (self.__doc['unit'], self.__doc['organization']), data = self.__doc['pk_org_unit'])
699
700 if self.__doc['unit_is_receiver']:
701 self._RBTN_org_is_receiver.Value = True
702 else:
703 self._RBTN_org_is_source.Value = True
704
705 if self.__reviewing_doc:
706 self._PRW_org.Enable()
707 else:
708 self._PRW_org.Disable()
709
710 if self.__doc['pk_hospital_stay'] is not None:
711 self._PRW_hospital_stay.SetText(data = self.__doc['pk_hospital_stay'])
712
713 fts = gmDateTime.cFuzzyTimestamp(timestamp = self.__doc['clin_when'])
714 self._PhWheel_doc_date.SetText(fts.strftime('%Y-%m-%d'), fts)
715 self._TCTRL_reference.SetValue(gmTools.coalesce(self.__doc['ext_ref'], ''))
716 if self.__reviewing_doc:
717 self._TCTRL_filename.Enable(False)
718 self._SPINCTRL_seq_idx.Enable(False)
719 else:
720 self._TCTRL_filename.SetValue(gmTools.coalesce(self.__part['filename'], ''))
721 self._SPINCTRL_seq_idx.SetValue(gmTools.coalesce(self.__part['seq_idx'], 0))
722
723 self._LCTRL_existing_reviews.InsertColumn(0, _('who'))
724 self._LCTRL_existing_reviews.InsertColumn(1, _('when'))
725 self._LCTRL_existing_reviews.InsertColumn(2, _('+/-'))
726 self._LCTRL_existing_reviews.InsertColumn(3, _('!'))
727 self._LCTRL_existing_reviews.InsertColumn(4, _('comment'))
728
729 self.__reload_existing_reviews()
730
731 if self._LCTRL_existing_reviews.GetItemCount() > 0:
732 self._LCTRL_existing_reviews.SetColumnWidth(0, wx.LIST_AUTOSIZE)
733 self._LCTRL_existing_reviews.SetColumnWidth(1, wx.LIST_AUTOSIZE)
734 self._LCTRL_existing_reviews.SetColumnWidth(2, wx.LIST_AUTOSIZE_USEHEADER)
735 self._LCTRL_existing_reviews.SetColumnWidth(3, wx.LIST_AUTOSIZE_USEHEADER)
736 self._LCTRL_existing_reviews.SetColumnWidth(4, wx.LIST_AUTOSIZE)
737
738 if self.__part is None:
739 self._ChBOX_review.SetValue(False)
740 self._ChBOX_review.Enable(False)
741 self._ChBOX_abnormal.Enable(False)
742 self._ChBOX_relevant.Enable(False)
743 self._ChBOX_sign_all_pages.Enable(False)
744 else:
745 me = gmStaff.gmCurrentProvider()
746 if self.__part['pk_intended_reviewer'] == me['pk_staff']:
747 msg = _('(you are the primary reviewer)')
748 else:
749 other = gmStaff.cStaff(aPK_obj = self.__part['pk_intended_reviewer'])
750 msg = _('(someone else is the intended reviewer: %s)') % other['short_alias']
751 self._TCTRL_responsible.SetValue(msg)
752
753 if self.__part['reviewed_by_you']:
754 revs = self.__part.get_reviews()
755 for rev in revs:
756 if rev['is_your_review']:
757 self._ChBOX_abnormal.SetValue(bool(rev[2]))
758 self._ChBOX_relevant.SetValue(bool(rev[3]))
759 break
760
761 self._ChBOX_sign_all_pages.SetValue(self.__reviewing_doc)
762
763 return True
764
765
767 self._LCTRL_existing_reviews.DeleteAllItems()
768 if self.__part is None:
769 return True
770 revs = self.__part.get_reviews()
771 if len(revs) == 0:
772 return True
773
774 review_by_responsible_doc = None
775 reviews_by_others = []
776 for rev in revs:
777 if rev['is_review_by_responsible_reviewer'] and not rev['is_your_review']:
778 review_by_responsible_doc = rev
779 if not (rev['is_review_by_responsible_reviewer'] or rev['is_your_review']):
780 reviews_by_others.append(rev)
781
782 if review_by_responsible_doc is not None:
783 row_num = self._LCTRL_existing_reviews.InsertItem(sys.maxsize, label=review_by_responsible_doc[0])
784 self._LCTRL_existing_reviews.SetItemTextColour(row_num, column=wx.BLUE)
785 self._LCTRL_existing_reviews.SetItem(index = row_num, column=0, label=review_by_responsible_doc[0])
786 self._LCTRL_existing_reviews.SetItem(index = row_num, column=1, label=review_by_responsible_doc[1].strftime('%x %H:%M'))
787 if review_by_responsible_doc['is_technically_abnormal']:
788 self._LCTRL_existing_reviews.SetItem(index = row_num, column=2, label='X')
789 if review_by_responsible_doc['clinically_relevant']:
790 self._LCTRL_existing_reviews.SetItem(index = row_num, column=3, label='X')
791 self._LCTRL_existing_reviews.SetItem(index = row_num, column=4, label=review_by_responsible_doc[6])
792 row_num += 1
793 for rev in reviews_by_others:
794 row_num = self._LCTRL_existing_reviews.InsertItem(sys.maxsize, label=rev[0])
795 self._LCTRL_existing_reviews.SetItem(index = row_num, column=0, label=rev[0])
796 self._LCTRL_existing_reviews.SetItem(index = row_num, column=1, label=rev[1].strftime('%x %H:%M'))
797 if rev['is_technically_abnormal']:
798 self._LCTRL_existing_reviews.SetItem(index = row_num, column=2, label='X')
799 if rev['clinically_relevant']:
800 self._LCTRL_existing_reviews.SetItem(index = row_num, column=3, label='X')
801 self._LCTRL_existing_reviews.SetItem(index = row_num, column=4, label=rev[6])
802 return True
803
804
805
806
903
904
906 state = self._ChBOX_review.GetValue()
907 self._ChBOX_abnormal.Enable(enable = state)
908 self._ChBOX_relevant.Enable(enable = state)
909 self._ChBOX_responsible.Enable(enable = state)
910
911
913 """Per Jim: Changing the doc type happens a lot more often
914 then correcting spelling, hence select-all on getting focus.
915 """
916 self._PhWheel_doc_type.SetSelection(-1, -1)
917
918
920 pk_doc_type = self._PhWheel_doc_type.GetData()
921 if pk_doc_type is None:
922 self._PRW_doc_comment.unset_context(context = 'pk_doc_type')
923 else:
924 self._PRW_doc_comment.set_context(context = 'pk_doc_type', val = pk_doc_type)
925 return True
926
927
929
930 _log.debug('acquiring images from [%s]', device)
931
932
933
934 from Gnumed.pycommon import gmScanBackend
935 try:
936 fnames = gmScanBackend.acquire_pages_into_files (
937 device = device,
938 delay = 5,
939 calling_window = calling_window
940 )
941 except OSError:
942 _log.exception('problem acquiring image from source')
943 gmGuiHelpers.gm_show_error (
944 aMessage = _(
945 'No images could be acquired from the source.\n\n'
946 'This may mean the scanner driver is not properly installed.\n\n'
947 'On Windows you must install the TWAIN Python module\n'
948 'while on Linux and MacOSX it is recommended to install\n'
949 'the XSane package.'
950 ),
951 aTitle = _('Acquiring images')
952 )
953 return None
954
955 _log.debug('acquired %s images', len(fnames))
956
957 return fnames
958
959
960 from Gnumed.wxGladeWidgets import wxgScanIdxPnl
961
962 -class cScanIdxDocsPnl(wxgScanIdxPnl.wxgScanIdxPnl, gmPlugin.cPatientChange_PluginMixin):
963
983
984
985
986
988 pat = gmPerson.gmCurrentPatient()
989 if not pat.connected:
990 gmDispatcher.send(signal='statustext', msg=_('Cannot accept new documents. No active patient.'))
991 return
992
993
994 real_filenames = []
995 for pathname in filenames:
996 try:
997 files = os.listdir(pathname)
998 source = _('directory dropped on client')
999 gmDispatcher.send(signal = 'statustext', msg = _('Extracting files from folder [%s] ...') % pathname)
1000 for filename in files:
1001 fullname = os.path.join(pathname, filename)
1002 if not os.path.isfile(fullname):
1003 continue
1004 real_filenames.append(fullname)
1005 except OSError:
1006 source = _('file dropped on client')
1007 real_filenames.append(pathname)
1008
1009 self.add_parts_from_files(real_filenames, source)
1010
1011
1014
1015
1016
1017
1021
1022
1023 - def _post_patient_selection(self, **kwds):
1024 self.__init_ui_data()
1025
1026
1027
1028
1065
1066
1075
1076
1078 title = _('saving document')
1079
1080 if self._LCTRL_doc_pages.ItemCount == 0:
1081 dbcfg = gmCfg.cCfgSQL()
1082 allow_empty = bool(dbcfg.get2 (
1083 option = 'horstspace.scan_index.allow_partless_documents',
1084 workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace,
1085 bias = 'user',
1086 default = False
1087 ))
1088 if allow_empty:
1089 save_empty = gmGuiHelpers.gm_show_question (
1090 aMessage = _('No parts to save. Really save an empty document as a reference ?'),
1091 aTitle = title
1092 )
1093 if not save_empty:
1094 return False
1095 else:
1096 gmGuiHelpers.gm_show_error (
1097 aMessage = _('No parts to save. Aquire some parts first.'),
1098 aTitle = title
1099 )
1100 return False
1101
1102 doc_type_pk = self._PhWheel_doc_type.GetData(can_create = True)
1103 if doc_type_pk is None:
1104 gmGuiHelpers.gm_show_error (
1105 aMessage = _('No document type applied. Choose a document type'),
1106 aTitle = title
1107 )
1108 return False
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118 if self._PhWheel_episode.GetValue().strip() == '':
1119 gmGuiHelpers.gm_show_error (
1120 aMessage = _('You must select an episode to save this document under.'),
1121 aTitle = title
1122 )
1123 return False
1124
1125 if self._PhWheel_reviewer.GetData() is None:
1126 gmGuiHelpers.gm_show_error (
1127 aMessage = _('You need to select from the list of staff members the doctor who is intended to sign the document.'),
1128 aTitle = title
1129 )
1130 return False
1131
1132 if self._PhWheel_doc_date.is_valid_timestamp(empty_is_valid = True) is False:
1133 gmGuiHelpers.gm_show_error (
1134 aMessage = _('Invalid date of generation.'),
1135 aTitle = title
1136 )
1137 return False
1138
1139 return True
1140
1141
1143
1144 if not reconfigure:
1145 dbcfg = gmCfg.cCfgSQL()
1146 device = dbcfg.get2 (
1147 option = 'external.xsane.default_device',
1148 workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace,
1149 bias = 'workplace',
1150 default = ''
1151 )
1152 if device.strip() == '':
1153 device = None
1154 if device is not None:
1155 return device
1156
1157 try:
1158 devices = self.scan_module.get_devices()
1159 except:
1160 _log.exception('cannot retrieve list of image sources')
1161 gmDispatcher.send(signal = 'statustext', msg = _('There is no scanner support installed on this machine.'))
1162 return None
1163
1164 if devices is None:
1165
1166
1167 return None
1168
1169 if len(devices) == 0:
1170 gmDispatcher.send(signal = 'statustext', msg = _('Cannot find an active scanner.'))
1171 return None
1172
1173
1174
1175
1176
1177 device = gmListWidgets.get_choices_from_list (
1178 parent = self,
1179 msg = _('Select an image capture device'),
1180 caption = _('device selection'),
1181 choices = [ '%s (%s)' % (d[2], d[0]) for d in devices ],
1182 columns = [_('Device')],
1183 data = devices,
1184 single_selection = True
1185 )
1186 if device is None:
1187 return None
1188
1189
1190 return device[0]
1191
1192
1193
1194
1196
1197 chosen_device = self.get_device_to_use()
1198
1199
1200
1201 try:
1202 fnames = self.scan_module.acquire_pages_into_files (
1203 device = chosen_device,
1204 delay = 5,
1205 calling_window = self
1206 )
1207 except OSError:
1208 _log.exception('problem acquiring image from source')
1209 gmGuiHelpers.gm_show_error (
1210 aMessage = _(
1211 'No pages could be acquired from the source.\n\n'
1212 'This may mean the scanner driver is not properly installed.\n\n'
1213 'On Windows you must install the TWAIN Python module\n'
1214 'while on Linux and MacOSX it is recommended to install\n'
1215 'the XSane package.'
1216 ),
1217 aTitle = _('acquiring page')
1218 )
1219 return None
1220
1221 if len(fnames) == 0:
1222 return True
1223
1224 self.add_parts_from_files(fnames, _('captured by imaging device'))
1225 return True
1226
1227
1229
1230 dlg = wx.FileDialog (
1231 parent = None,
1232 message = _('Choose a file'),
1233 defaultDir = os.path.expanduser(os.path.join('~', 'gnumed')),
1234 defaultFile = '',
1235 wildcard = "%s (*)|*|TIFFs (*.tif)|*.tif|JPEGs (*.jpg)|*.jpg|%s (*.*)|*.*" % (_('all files'), _('all files (Win)')),
1236 style = wx.FD_OPEN | wx.FD_FILE_MUST_EXIST | wx.FD_MULTIPLE
1237 )
1238 result = dlg.ShowModal()
1239 files = dlg.GetPaths()
1240 if result == wx.ID_CANCEL:
1241 dlg.Destroy()
1242 return
1243
1244 self.add_parts_from_files(files, _('picked from storage media'))
1245
1246
1255
1256
1258
1259
1260 if self._LCTRL_doc_pages.ItemCount == 0:
1261 return
1262
1263
1264 if self._LCTRL_doc_pages.ItemCount == 1:
1265 page_fnames = [ self._LCTRL_doc_pages.get_item_data(0)[0] ]
1266 else:
1267
1268 page_fnames = [ data[0] for data in self._LCTRL_doc_pages.selected_item_data ]
1269 if len(page_fnames) == 0:
1270 gmDispatcher.send(signal = 'statustext', msg = _('No part selected for viewing.'), beep = True)
1271 return
1272
1273 for page_fname in page_fnames:
1274 (success, msg) = gmMimeLib.call_viewer_on_file(page_fname)
1275 if not success:
1276 gmGuiHelpers.gm_show_warning (
1277 aMessage = _('Cannot display document part:\n%s') % msg,
1278 aTitle = _('displaying part')
1279 )
1280
1281
1283
1284 if len(self._LCTRL_doc_pages.selected_items) == 0:
1285 gmDispatcher.send(signal = 'statustext', msg = _('No part selected for removal.'), beep = True)
1286 return
1287
1288 sel_idx = self._LCTRL_doc_pages.GetFirstSelected()
1289 rows = self._LCTRL_doc_pages.string_items
1290 data = self._LCTRL_doc_pages.data
1291 del rows[sel_idx]
1292 del data[sel_idx]
1293 self._LCTRL_doc_pages.string_items = rows
1294 self._LCTRL_doc_pages.data = data
1295 self._LCTRL_doc_pages.set_column_widths()
1296 self._TCTRL_metadata.SetValue('')
1297
1298
1300
1301 if not self.__valid_for_save():
1302 return False
1303
1304
1305 cfg = gmCfg.cCfgSQL()
1306 generate_uuid = bool (
1307 cfg.get2 (
1308 option = 'horstspace.scan_index.generate_doc_uuid',
1309 workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace,
1310 bias = 'user',
1311 default = False
1312 )
1313 )
1314 if generate_uuid:
1315 ext_ref = gmDocuments.get_ext_ref()
1316 else:
1317 ext_ref = None
1318
1319
1320 new_doc = save_files_as_new_document (
1321 parent = self,
1322 filenames = [ data[0] for data in self._LCTRL_doc_pages.data ],
1323 document_type = self._PhWheel_doc_type.GetValue().strip(),
1324 pk_document_type = self._PhWheel_doc_type.GetData(),
1325 unlock_patient = False,
1326 episode = self._PhWheel_episode.GetData(can_create = True, is_open = True, as_instance = True),
1327 review_as_normal = False,
1328 reference = ext_ref,
1329 pk_org_unit = self._PhWheel_source.GetData(),
1330
1331 date_generated = gmTools.coalesce (
1332 self._PhWheel_doc_date.GetData(),
1333 function_initial = 'get_pydt'
1334 ),
1335 comment = self._PRW_doc_comment.GetLineText(0).strip(),
1336 reviewer = self._PhWheel_reviewer.GetData()
1337 )
1338 if new_doc is None:
1339 return False
1340
1341 if self._RBTN_org_is_receiver.Value is True:
1342 new_doc['unit_is_receiver'] = True
1343 new_doc.save()
1344
1345
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
1593
1594 if (document is None) and (part is None):
1595 self._LCTRL_details.set_string_items([])
1596 return
1597
1598 if document is None:
1599 document = part.document
1600
1601 items = []
1602 if document is not None:
1603 items.append([_('Document'), '%s [#%s]' % (document['l10n_type'], document['pk_doc'])])
1604 items.append([_('Generated'), gmDateTime.pydt_strftime(document['clin_when'], '%Y %b %d')])
1605 items.append([_('Health issue'), gmTools.coalesce(document['health_issue'], '', '%%s [#%s]' % document['pk_health_issue'])])
1606 items.append([_('Episode'), '%s (%s) [#%s]' % (
1607 document['episode'],
1608 gmTools.bool2subst(document['episode_open'], _('open'), _('closed')),
1609 document['pk_episode']
1610 )])
1611 if document['pk_org_unit'] is not None:
1612 if document['unit_is_receiver']:
1613 header = _('Receiver')
1614 else:
1615 header = _('Sender')
1616 items.append([header, '%s @ %s' % (document['unit'], document['organization'])])
1617 if document['ext_ref'] is not None:
1618 items.append([_('Reference'), document['ext_ref']])
1619 if document['comment'] is not None:
1620 items.append([_('Comment'), ' / '.join(document['comment'].split('\n'))])
1621 for proc in document.procedures:
1622 items.append([_('Procedure'), proc.format (
1623 left_margin = 0,
1624 include_episode = False,
1625 include_codes = False,
1626 include_address = False,
1627 include_comm = False,
1628 include_doc = False
1629 )])
1630 stay = document.hospital_stay
1631 if stay is not None:
1632 items.append([_('Hospital stay'), stay.format(include_episode = False)])
1633 for bill in document.bills:
1634 items.append([_('Bill'), bill.format (
1635 include_receiver = False,
1636 include_doc = False
1637 )])
1638 items.append([_('Modified'), gmDateTime.pydt_strftime(document['modified_when'], '%Y %b %d')])
1639 items.append([_('... by'), document['modified_by']])
1640 items.append([_('# encounter'), document['pk_encounter']])
1641
1642 if part is not None:
1643 items.append(['', ''])
1644 if part['seq_idx'] is None:
1645 items.append([_('Part'), '#%s' % part['pk_obj']])
1646 else:
1647 items.append([_('Part'), '%s [#%s]' % (part['seq_idx'], part['pk_obj'])])
1648 if part['obj_comment'] is not None:
1649 items.append([_('Comment'), part['obj_comment']])
1650 if part['filename'] is not None:
1651 items.append([_('Filename'), part['filename']])
1652 items.append([_('Data size'), gmTools.size2str(part['size'])])
1653 review_parts = []
1654 if part['reviewed_by_you']:
1655 review_parts.append(_('by you'))
1656 if part['reviewed_by_intended_reviewer']:
1657 review_parts.append(_('by intended reviewer'))
1658 review = ', '.join(review_parts)
1659 if review == '':
1660 review = gmTools.u_diameter
1661 items.append([_('Reviewed'), review])
1662
1663
1664 self._LCTRL_details.set_string_items(items)
1665 self._LCTRL_details.set_column_widths()
1666 self._LCTRL_details.set_resize_column(1)
1667
1668
1669 -class cDocTree(wx.TreeCtrl, gmRegetMixin.cRegetOnPaintMixin, treemixin.ExpansionState):
1670 """This wx.TreeCtrl derivative displays a tree view of stored medical documents.
1671
1672 It listens to document and patient changes and updates itself accordingly.
1673
1674 This acts on the current patient.
1675 """
1676 _sort_modes = ['age', 'review', 'episode', 'type', 'issue', 'org']
1677 _root_node_labels = None
1678
1679
1680 - def __init__(self, parent, id, *args, **kwds):
1681 """Set up our specialised tree.
1682 """
1683 kwds['style'] = wx.TR_NO_BUTTONS | wx.NO_BORDER | wx.TR_SINGLE
1684 wx.TreeCtrl.__init__(self, parent, id, *args, **kwds)
1685
1686 gmRegetMixin.cRegetOnPaintMixin.__init__(self)
1687
1688 tmp = _('available documents (%s)')
1689 unsigned = _('unsigned (%s) on top') % '\u270D'
1690 cDocTree._root_node_labels = {
1691 'age': tmp % _('most recent on top'),
1692 'review': tmp % unsigned,
1693 'episode': tmp % _('sorted by episode'),
1694 'issue': tmp % _('sorted by health issue'),
1695 'type': tmp % _('sorted by type'),
1696 'org': tmp % _('sorted by organization')
1697 }
1698
1699 self.root = None
1700 self.__sort_mode = 'age'
1701
1702 self.__expanded_nodes = None
1703 self.__show_details_callback = None
1704
1705 self.__build_context_menus()
1706 self.__register_interests()
1707 self._schedule_data_reget()
1708
1709
1710
1711
1713
1714 node = self.GetSelection()
1715 node_data = self.GetItemData(node)
1716
1717 if not isinstance(node_data, gmDocuments.cDocumentPart):
1718 return True
1719
1720 self.__display_part(part = node_data)
1721 return True
1722
1723
1724
1725
1727 return self.__sort_mode
1728
1747
1748 sort_mode = property(_get_sort_mode, _set_sort_mode)
1749
1750
1752 if callback is not None:
1753 if not callable(callback):
1754 raise ValueError('<%s> is not callable')
1755 self.__show_details_callback = callback
1756
1757 show_details_callback = property(lambda x:x, _set_show_details_callback)
1758
1759
1760
1761
1763 curr_pat = gmPerson.gmCurrentPatient()
1764 if not curr_pat.connected:
1765 gmDispatcher.send(signal = 'statustext', msg = _('Cannot load documents. No active patient.'))
1766 return False
1767
1768 if not self.__populate_tree():
1769 return False
1770
1771 return True
1772
1773
1774
1775
1777
1778 self.Bind(wx.EVT_TREE_SEL_CHANGED, self._on_tree_item_selected)
1779 self.Bind(wx.EVT_TREE_ITEM_ACTIVATED, self._on_activate)
1780 self.Bind(wx.EVT_TREE_ITEM_RIGHT_CLICK, self.__on_right_click)
1781 self.Bind(wx.EVT_TREE_ITEM_GETTOOLTIP, self._on_tree_item_gettooltip)
1782
1783
1784
1785
1786
1787
1788
1789 gmDispatcher.connect(signal = 'pre_patient_unselection', receiver = self._on_pre_patient_unselection)
1790 gmDispatcher.connect(signal = 'post_patient_selection', receiver = self._on_post_patient_selection)
1791 gmDispatcher.connect(signal = 'blobs.doc_med_mod_db', receiver = self._on_doc_mod_db)
1792 gmDispatcher.connect(signal = 'blobs.doc_obj_mod_db', receiver = self._on_doc_page_mod_db)
1793
1794
1796
1797
1798 self.__part_context_menu = wx.Menu(title = _('Part Actions:'))
1799
1800 item = self.__part_context_menu.Append(-1, _('Display part'))
1801 self.Bind(wx.EVT_MENU, self.__display_curr_part, item)
1802 item = self.__part_context_menu.Append(-1, _('%s Sign/Edit properties') % '\u270D')
1803 self.Bind(wx.EVT_MENU, self.__review_curr_part, item)
1804
1805 self.__part_context_menu.AppendSeparator()
1806
1807 item = self.__part_context_menu.Append(-1, _('Delete part'))
1808 self.Bind(wx.EVT_MENU, self.__delete_part, item, item)
1809 item = self.__part_context_menu.Append(-1, _('Move part'))
1810 self.Bind(wx.EVT_MENU, self.__move_part, item)
1811 item = self.__part_context_menu.Append(-1, _('Print part'))
1812 self.Bind(wx.EVT_MENU, self.__print_part, item)
1813 item = self.__part_context_menu.Append(-1, _('Fax part'))
1814 self.Bind(wx.EVT_MENU, self.__fax_part, item)
1815 item = self.__part_context_menu.Append(-1, _('Mail part'))
1816 self.Bind(wx.EVT_MENU, self.__mail_part, item)
1817 item = self.__part_context_menu.Append(-1, _('Save part to disk'))
1818 self.Bind(wx.EVT_MENU, self.__save_part_to_disk, item)
1819
1820 self.__part_context_menu.AppendSeparator()
1821
1822
1823 self.__doc_context_menu = wx.Menu(title = _('Document Actions:'))
1824
1825 item = self.__doc_context_menu.Append(-1, _('%s Sign/Edit properties') % '\u270D')
1826 self.Bind(wx.EVT_MENU, self.__review_curr_part, item)
1827 item = self.__doc_context_menu.Append(-1, _('Delete document'))
1828 self.Bind(wx.EVT_MENU, self.__delete_document, item)
1829
1830 self.__doc_context_menu.AppendSeparator()
1831
1832 item = self.__doc_context_menu.Append(-1, _('Add parts'))
1833 self.Bind(wx.EVT_MENU, self.__add_part, item)
1834 item = self.__doc_context_menu.Append(-1, _('Add part from clipboard'))
1835 self.Bind(wx.EVT_MENU, self.__add_part_from_clipboard, item, item)
1836 item = self.__doc_context_menu.Append(-1, _('Print all parts'))
1837 self.Bind(wx.EVT_MENU, self.__print_doc, item)
1838 item = self.__doc_context_menu.Append(-1, _('Fax all parts'))
1839 self.Bind(wx.EVT_MENU, self.__fax_doc, item)
1840 item = self.__doc_context_menu.Append(-1, _('Mail all parts'))
1841 self.Bind(wx.EVT_MENU, self.__mail_doc, item)
1842 item = self.__doc_context_menu.Append(-1, _('Save all parts to disk'))
1843 self.Bind(wx.EVT_MENU, self.__save_doc_to_disk, item)
1844 item = self.__doc_context_menu.Append(-1, _('Copy all parts to export area'))
1845 self.Bind(wx.EVT_MENU, self.__copy_doc_to_export_area, item)
1846
1847 self.__doc_context_menu.AppendSeparator()
1848
1849 item = self.__doc_context_menu.Append(-1, _('Access external original'))
1850 self.Bind(wx.EVT_MENU, self.__access_external_original, item)
1851 item = self.__doc_context_menu.Append(-1, _('Edit corresponding encounter'))
1852 self.Bind(wx.EVT_MENU, self.__edit_encounter_details, item)
1853 item = self.__doc_context_menu.Append(-1, _('Select corresponding encounter'))
1854 self.Bind(wx.EVT_MENU, self.__select_encounter, item)
1855 item = self.__doc_context_menu.Append(-1, _('Manage descriptions'))
1856 self.Bind(wx.EVT_MENU, self.__manage_document_descriptions, item)
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1869
1870 wx.BeginBusyCursor()
1871
1872
1873 if self.root is not None:
1874 self.DeleteAllItems()
1875
1876
1877 self.root = self.AddRoot(cDocTree._root_node_labels[self.__sort_mode], -1, -1)
1878 self.SetItemData(self.root, None)
1879 self.SetItemHasChildren(self.root, False)
1880
1881
1882 curr_pat = gmPerson.gmCurrentPatient()
1883 docs_folder = curr_pat.get_document_folder()
1884 docs = docs_folder.get_documents()
1885
1886 if docs is None:
1887 gmGuiHelpers.gm_show_error (
1888 aMessage = _('Error searching documents.'),
1889 aTitle = _('loading document list')
1890 )
1891
1892 wx.EndBusyCursor()
1893 return True
1894
1895 if len(docs) == 0:
1896 wx.EndBusyCursor()
1897 return True
1898
1899
1900 self.SetItemHasChildren(self.root, True)
1901
1902
1903 intermediate_nodes = {}
1904 for doc in docs:
1905
1906 parts = doc.parts
1907
1908 if len(parts) == 0:
1909 no_parts = _('no parts')
1910 elif len(parts) == 1:
1911 no_parts = _('1 part')
1912 else:
1913 no_parts = _('%s parts') % len(parts)
1914
1915
1916 if self.__sort_mode == 'episode':
1917 intermediate_label = '%s%s' % (doc['episode'], gmTools.coalesce(doc['health_issue'], '', ' (%s)'))
1918 doc_label = _('%s%7s %s:%s (%s)') % (
1919 gmTools.bool2subst(doc.has_unreviewed_parts, gmTools.u_writing_hand, '', '?'),
1920 doc['clin_when'].strftime('%m/%Y'),
1921 doc['l10n_type'][:26],
1922 gmTools.coalesce(initial = doc['comment'], instead = '', template_initial = ' %s'),
1923 no_parts
1924 )
1925 if intermediate_label not in intermediate_nodes:
1926 intermediate_nodes[intermediate_label] = self.AppendItem(parent = self.root, text = intermediate_label)
1927 self.SetItemBold(intermediate_nodes[intermediate_label], bold = True)
1928 self.SetItemData(intermediate_nodes[intermediate_label], None)
1929 self.SetItemHasChildren(intermediate_nodes[intermediate_label], True)
1930 parent = intermediate_nodes[intermediate_label]
1931
1932 elif self.__sort_mode == 'type':
1933 intermediate_label = doc['l10n_type']
1934 doc_label = _('%s%7s (%s):%s') % (
1935 gmTools.bool2subst(doc.has_unreviewed_parts, gmTools.u_writing_hand, '', '?'),
1936 doc['clin_when'].strftime('%m/%Y'),
1937 no_parts,
1938 gmTools.coalesce(initial = doc['comment'], instead = '', template_initial = ' %s')
1939 )
1940 if intermediate_label not in intermediate_nodes:
1941 intermediate_nodes[intermediate_label] = self.AppendItem(parent = self.root, text = intermediate_label)
1942 self.SetItemBold(intermediate_nodes[intermediate_label], bold = True)
1943 self.SetItemData(intermediate_nodes[intermediate_label], None)
1944 self.SetItemHasChildren(intermediate_nodes[intermediate_label], True)
1945 parent = intermediate_nodes[intermediate_label]
1946
1947 elif self.__sort_mode == 'issue':
1948 if doc['health_issue'] is None:
1949 intermediate_label = _('%s (unattributed episode)') % doc['episode']
1950 else:
1951 intermediate_label = doc['health_issue']
1952 doc_label = _('%s%7s %s:%s (%s)') % (
1953 gmTools.bool2subst(doc.has_unreviewed_parts, gmTools.u_writing_hand, '', '?'),
1954 doc['clin_when'].strftime('%m/%Y'),
1955 doc['l10n_type'][:26],
1956 gmTools.coalesce(initial = doc['comment'], instead = '', template_initial = ' %s'),
1957 no_parts
1958 )
1959 if intermediate_label not in intermediate_nodes:
1960 intermediate_nodes[intermediate_label] = self.AppendItem(parent = self.root, text = intermediate_label)
1961 self.SetItemBold(intermediate_nodes[intermediate_label], bold = True)
1962 self.SetItemData(intermediate_nodes[intermediate_label], None)
1963 self.SetItemHasChildren(intermediate_nodes[intermediate_label], True)
1964 parent = intermediate_nodes[intermediate_label]
1965
1966 elif self.__sort_mode == 'org':
1967 if doc['pk_org'] is None:
1968 intermediate_label = _('unknown organization')
1969 tt = ''
1970 else:
1971 if doc['unit_is_receiver']:
1972 direction = _('to: %s')
1973 else:
1974 direction = _('from: %s')
1975
1976 if doc['pk_org'] == gmPraxis.gmCurrentPraxisBranch()['pk_org']:
1977 org_str = _('this praxis')
1978 else:
1979 org_str = doc['organization']
1980 intermediate_label = direction % org_str
1981
1982 tt = '\n'.join(doc.org_unit.format(with_address = True, with_org = True, with_comms = True))
1983 doc_label = _('%s%7s %s:%s (%s)') % (
1984 gmTools.bool2subst(doc.has_unreviewed_parts, gmTools.u_writing_hand, '', '?'),
1985 doc['clin_when'].strftime('%m/%Y'),
1986 doc['l10n_type'][:26],
1987 gmTools.coalesce(initial = doc['comment'], instead = '', template_initial = ' %s'),
1988 no_parts
1989 )
1990 if intermediate_label not in intermediate_nodes:
1991 intermediate_nodes[intermediate_label] = self.AppendItem(parent = self.root, text = intermediate_label)
1992 self.SetItemBold(intermediate_nodes[intermediate_label], bold = True)
1993
1994 self.SetItemData(intermediate_nodes[intermediate_label], tt)
1995 self.SetItemHasChildren(intermediate_nodes[intermediate_label], True)
1996 parent = intermediate_nodes[intermediate_label]
1997
1998 else:
1999 doc_label = _('%s%7s %s:%s (%s)') % (
2000 gmTools.bool2subst(doc.has_unreviewed_parts, gmTools.u_writing_hand, '', '?'),
2001 doc['clin_when'].strftime('%Y-%m'),
2002 doc['l10n_type'][:26],
2003 gmTools.coalesce(initial = doc['comment'], instead = '', template_initial = ' %s'),
2004 no_parts
2005 )
2006 parent = self.root
2007
2008 doc_node = self.AppendItem(parent = parent, text = doc_label)
2009
2010 self.SetItemData(doc_node, doc)
2011 if len(parts) == 0:
2012 self.SetItemHasChildren(doc_node, False)
2013 else:
2014 self.SetItemHasChildren(doc_node, True)
2015
2016
2017 for part in parts:
2018 f_ext = ''
2019 if part['filename'] is not None:
2020 f_ext = os.path.splitext(part['filename'])[1].strip('.').strip()
2021 if f_ext != '':
2022 f_ext = ' .' + f_ext.upper()
2023 label = '%s%s (%s%s)%s' % (
2024 gmTools.bool2str (
2025 boolean = part['reviewed'] or part['reviewed_by_you'] or part['reviewed_by_intended_reviewer'],
2026 true_str = '',
2027 false_str = gmTools.u_writing_hand
2028 ),
2029 _('part %2s') % part['seq_idx'],
2030 gmTools.size2str(part['size']),
2031 f_ext,
2032 gmTools.coalesce (
2033 part['obj_comment'],
2034 '',
2035 ': %s%%s%s' % (gmTools.u_left_double_angle_quote, gmTools.u_right_double_angle_quote)
2036 )
2037 )
2038
2039 part_node = self.AppendItem(parent = doc_node, text = label)
2040 self.SetItemData(part_node, part)
2041 self.SetItemHasChildren(part_node, False)
2042
2043 self.__sort_nodes()
2044 self.SelectItem(self.root)
2045
2046
2047 if self.__expanded_nodes is not None:
2048 self.ExpansionState = self.__expanded_nodes
2049
2050 self.Expand(self.root)
2051
2052
2053 if self.__expanded_nodes is None:
2054
2055 if self.__sort_mode in ['episode', 'type', 'issue', 'org']:
2056 for key in intermediate_nodes.keys():
2057 self.Expand(intermediate_nodes[key])
2058
2059 wx.EndBusyCursor()
2060
2061 return True
2062
2063
2065 """Used in sorting items.
2066
2067 -1: 1 < 2
2068 0: 1 = 2
2069 1: 1 > 2
2070 """
2071
2072 if not node1:
2073 _log.debug('invalid node 1')
2074 return 0
2075 if not node2:
2076 _log.debug('invalid node 2')
2077 return 0
2078 if not node1.IsOk():
2079 _log.debug('no data on node 1')
2080 return 0
2081 if not node2.IsOk():
2082 _log.debug('no data on node 2')
2083 return 0
2084
2085 data1 = self.GetItemData(node1)
2086 data2 = self.GetItemData(node2)
2087
2088
2089 if isinstance(data1, gmDocuments.cDocument):
2090
2091 date_field = 'clin_when'
2092
2093
2094 if self.__sort_mode == 'age':
2095
2096 if data1[date_field] > data2[date_field]:
2097 return -1
2098 if data1[date_field] == data2[date_field]:
2099 return 0
2100 return 1
2101
2102 elif self.__sort_mode == 'episode':
2103 if data1['episode'] < data2['episode']:
2104 return -1
2105 if data1['episode'] == data2['episode']:
2106
2107 if data1[date_field] > data2[date_field]:
2108 return -1
2109 if data1[date_field] == data2[date_field]:
2110 return 0
2111 return 1
2112 return 1
2113
2114 elif self.__sort_mode == 'issue':
2115 if data1['health_issue'] < data2['health_issue']:
2116 return -1
2117 if data1['health_issue'] == data2['health_issue']:
2118
2119 if data1[date_field] > data2[date_field]:
2120 return -1
2121 if data1[date_field] == data2[date_field]:
2122 return 0
2123 return 1
2124 return 1
2125
2126 elif self.__sort_mode == 'review':
2127
2128 if data1.has_unreviewed_parts == data2.has_unreviewed_parts:
2129
2130 if data1[date_field] > data2[date_field]:
2131 return -1
2132 if data1[date_field] == data2[date_field]:
2133 return 0
2134 return 1
2135 if data1.has_unreviewed_parts:
2136 return -1
2137 return 1
2138
2139 elif self.__sort_mode == 'type':
2140 if data1['l10n_type'] < data2['l10n_type']:
2141 return -1
2142 if data1['l10n_type'] == data2['l10n_type']:
2143
2144 if data1[date_field] > data2[date_field]:
2145 return -1
2146 if data1[date_field] == data2[date_field]:
2147 return 0
2148 return 1
2149 return 1
2150
2151 elif self.__sort_mode == 'org':
2152 if (data1['organization'] is None) and (data2['organization'] is None):
2153 return 0
2154 if (data1['organization'] is None) and (data2['organization'] is not None):
2155 return 1
2156 if (data1['organization'] is not None) and (data2['organization'] is None):
2157 return -1
2158 txt1 = '%s %s' % (data1['organization'], data1['unit'])
2159 txt2 = '%s %s' % (data2['organization'], data2['unit'])
2160 if txt1 < txt2:
2161 return -1
2162 if txt1 == txt2:
2163
2164 if data1[date_field] > data2[date_field]:
2165 return -1
2166 if data1[date_field] == data2[date_field]:
2167 return 0
2168 return 1
2169 return 1
2170
2171 else:
2172 _log.error('unknown document sort mode [%s], reverse-sorting by age', self.__sort_mode)
2173
2174 if data1[date_field] > data2[date_field]:
2175 return -1
2176 if data1[date_field] == data2[date_field]:
2177 return 0
2178 return 1
2179
2180
2181 if isinstance(data1, gmDocuments.cDocumentPart):
2182
2183
2184 if data1['seq_idx'] < data2['seq_idx']:
2185 return -1
2186 if data1['seq_idx'] == data2['seq_idx']:
2187 return 0
2188 return 1
2189
2190
2191 if None in [data1, data2]:
2192 l1 = self.GetItemText(node1)
2193 l2 = self.GetItemText(node2)
2194 if l1 < l2:
2195 return -1
2196 if l1 == l2:
2197 return 0
2198 else:
2199 if data1 < data2:
2200 return -1
2201 if data1 == data2:
2202 return 0
2203 return 1
2204
2205
2206
2207
2209 self.__expanded_nodes = self.ExpansionState
2210 self._schedule_data_reget()
2211
2212 - def _on_doc_page_mod_db(self, *args, **kwargs):
2213 self.__expanded_nodes = self.ExpansionState
2214 self._schedule_data_reget()
2215
2217
2218 if self.root is not None:
2219 self.DeleteAllItems()
2220 self.root = None
2221
2222 - def _on_post_patient_selection(self, *args, **kwargs):
2223
2224 self.__expanded_nodes = None
2225 self._schedule_data_reget()
2226
2227
2229 node = event.GetItem()
2230 node_data = self.GetItemData(node)
2231
2232
2233 if node_data is None:
2234 self.__show_details_callback(document = None, part = None)
2235 return
2236
2237
2238 if isinstance(node_data, gmDocuments.cDocument):
2239 self.__show_details_callback(document = node_data, part = None)
2240 return
2241
2242
2243 if isinstance(node_data, str):
2244 self.__show_details_callback(document = None, part = None)
2245 return
2246
2247 if isinstance(node_data, gmDocuments.cDocumentPart):
2248 doc = self.GetItemData(self.GetItemParent(node))
2249 self.__show_details_callback(document = doc, part = node_data)
2250 return
2251
2252 raise ValueError(_('invalid document tree node data type: %s') % type(node_data))
2253
2254
2256 node = event.GetItem()
2257 node_data = self.GetItemData(node)
2258
2259
2260 if node_data is None:
2261 return None
2262
2263
2264 if isinstance(node_data, gmDocuments.cDocument):
2265 self.Toggle(node)
2266 return True
2267
2268
2269 if isinstance(node_data, str):
2270 self.Toggle(node)
2271 return True
2272
2273 if isinstance(node_data, gmDocuments.cDocumentPart):
2274 self.__display_part(part = node_data)
2275 return True
2276
2277 raise ValueError(_('invalid document tree node data type: %s') % type(node_data))
2278
2279
2281
2282 node = evt.GetItem()
2283 self.__curr_node_data = self.GetItemData(node)
2284
2285
2286 if self.__curr_node_data is None:
2287 return None
2288
2289
2290 if isinstance(self.__curr_node_data, gmDocuments.cDocument):
2291 self.__handle_doc_context()
2292
2293
2294 if isinstance(self.__curr_node_data, gmDocuments.cDocumentPart):
2295 self.__handle_part_context()
2296
2297 del self.__curr_node_data
2298 evt.Skip()
2299
2300
2303
2305 self.__display_part(part = self.__curr_node_data)
2306
2308 self.__review_part(part = self.__curr_node_data)
2309
2312
2337
2338
2339
2341
2342 if start_node is None:
2343 start_node = self.GetRootItem()
2344
2345
2346
2347 if not start_node.IsOk():
2348 return True
2349
2350 self.SortChildren(start_node)
2351
2352 child_node, cookie = self.GetFirstChild(start_node)
2353 while child_node.IsOk():
2354 self.__sort_nodes(start_node = child_node)
2355 child_node, cookie = self.GetNextChild(start_node, cookie)
2356
2357 return
2358
2360 self.PopupMenu(self.__doc_context_menu, wx.DefaultPosition)
2361
2362
2364 ID = None
2365
2366 if self.__curr_node_data['type'] == 'patient photograph':
2367 item = self.__part_context_menu.Append(-1, _('Activate as current photo'))
2368 self.Bind(wx.EVT_MENU, self.__activate_as_current_photo, item)
2369 ID = item.Id
2370
2371 self.PopupMenu(self.__part_context_menu, wx.DefaultPosition)
2372
2373 if ID is not None:
2374 self.__part_context_menu.Delete(ID)
2375
2376
2377
2378
2380 """Display document part."""
2381
2382
2383 if part['size'] == 0:
2384 _log.debug('cannot display part [%s] - 0 bytes', part['pk_obj'])
2385 gmGuiHelpers.gm_show_error (
2386 aMessage = _('Document part does not seem to exist in database !'),
2387 aTitle = _('showing document')
2388 )
2389 return None
2390
2391 wx.BeginBusyCursor()
2392
2393 cfg = gmCfg.cCfgSQL()
2394
2395
2396 chunksize = int(
2397 cfg.get2 (
2398 option = "horstspace.blob_export_chunk_size",
2399 workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace,
2400 bias = 'workplace',
2401 default = default_chunksize
2402 ))
2403
2404
2405 block_during_view = bool( cfg.get2 (
2406 option = 'horstspace.document_viewer.block_during_view',
2407 workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace,
2408 bias = 'user',
2409 default = None
2410 ))
2411
2412
2413 successful, msg = part.display_via_mime (
2414 chunksize = chunksize,
2415 block = block_during_view
2416 )
2417
2418 wx.EndBusyCursor()
2419
2420 if not successful:
2421 gmGuiHelpers.gm_show_error (
2422 aMessage = _('Cannot display document part:\n%s') % msg,
2423 aTitle = _('showing document')
2424 )
2425 return None
2426
2427
2428
2429
2430
2431
2432
2433 review_after_display = int(cfg.get2 (
2434 option = 'horstspace.document_viewer.review_after_display',
2435 workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace,
2436 bias = 'user',
2437 default = 3
2438 ))
2439 if review_after_display == 1:
2440 self.__review_part(part=part)
2441 elif review_after_display == 2:
2442 review_by_me = [ rev for rev in part.get_reviews() if rev['is_your_review'] ]
2443 if len(review_by_me) == 0:
2444 self.__review_part(part = part)
2445 elif review_after_display == 3:
2446 if len(part.get_reviews()) == 0:
2447 self.__review_part(part = part)
2448 elif review_after_display == 4:
2449 reviewed_by_responsible = [ rev for rev in part.get_reviews() if rev['is_review_by_responsible_reviewer'] ]
2450 if len(reviewed_by_responsible) == 0:
2451 self.__review_part(part = part)
2452
2453 return True
2454
2463
2465 target_doc = manage_documents (
2466 parent = self,
2467 msg = _('\nSelect the document into which to move the selected part !\n')
2468 )
2469 if target_doc is None:
2470 return
2471 if not self.__curr_node_data.reattach(pk_doc = target_doc['pk_doc']):
2472 gmGuiHelpers.gm_show_error (
2473 aMessage = _('Cannot move document part.'),
2474 aTitle = _('Moving document part')
2475 )
2476
2478 delete_it = gmGuiHelpers.gm_show_question (
2479 cancel_button = True,
2480 title = _('Deleting document part'),
2481 question = _(
2482 'Are you sure you want to delete the %s part #%s\n'
2483 '\n'
2484 '%s'
2485 'from the following document\n'
2486 '\n'
2487 ' %s (%s)\n'
2488 '%s'
2489 '\n'
2490 'Really delete ?\n'
2491 '\n'
2492 '(this action cannot be reversed)'
2493 ) % (
2494 gmTools.size2str(self.__curr_node_data['size']),
2495 self.__curr_node_data['seq_idx'],
2496 gmTools.coalesce(self.__curr_node_data['obj_comment'], '', ' "%s"\n\n'),
2497 self.__curr_node_data['l10n_type'],
2498 gmDateTime.pydt_strftime(self.__curr_node_data['date_generated'], format = '%Y-%m-%d', accuracy = gmDateTime.acc_days),
2499 gmTools.coalesce(self.__curr_node_data['doc_comment'], '', ' "%s"\n')
2500 )
2501 )
2502 if not delete_it:
2503 return
2504
2505 gmDocuments.delete_document_part (
2506 part_pk = self.__curr_node_data['pk_obj'],
2507 encounter_pk = gmPerson.gmCurrentPatient().emr.active_encounter['pk_encounter']
2508 )
2509
2511
2512 gmHooks.run_hook_script(hook = 'before_%s_doc_part' % action)
2513
2514 wx.BeginBusyCursor()
2515
2516
2517 found, external_cmd = gmShellAPI.detect_external_binary('gm-%s_doc' % action)
2518 if not found:
2519 found, external_cmd = gmShellAPI.detect_external_binary('gm-%s_doc.bat' % action)
2520 if not found:
2521 _log.error('neither of gm-%s_doc or gm-%s_doc.bat found', action, action)
2522 wx.EndBusyCursor()
2523 gmGuiHelpers.gm_show_error (
2524 _('Cannot %(l10n_action)s document part - %(l10n_action)s command not found.\n'
2525 '\n'
2526 'Either of gm-%(action)s_doc or gm-%(action)s_doc.bat\n'
2527 'must be in the execution path. The command will\n'
2528 'be passed the filename to %(l10n_action)s.'
2529 ) % {'action': action, 'l10n_action': l10n_action},
2530 _('Processing document part: %s') % l10n_action
2531 )
2532 return
2533
2534 cfg = gmCfg.cCfgSQL()
2535
2536
2537 chunksize = int(cfg.get2 (
2538 option = "horstspace.blob_export_chunk_size",
2539 workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace,
2540 bias = 'workplace',
2541 default = default_chunksize
2542 ))
2543
2544 part_file = self.__curr_node_data.save_to_file(aChunkSize = chunksize)
2545
2546 if action == 'print':
2547 cmd = '%s generic_document %s' % (external_cmd, part_file)
2548 else:
2549 cmd = '%s %s' % (external_cmd, part_file)
2550 if os.name == 'nt':
2551 blocking = True
2552 else:
2553 blocking = False
2554 success = gmShellAPI.run_command_in_shell (
2555 command = cmd,
2556 blocking = blocking
2557 )
2558
2559 wx.EndBusyCursor()
2560
2561 if not success:
2562 _log.error('%s command failed: [%s]', action, cmd)
2563 gmGuiHelpers.gm_show_error (
2564 _('Cannot %(l10n_action)s document part - %(l10n_action)s command failed.\n'
2565 '\n'
2566 'You may need to check and fix either of\n'
2567 ' gm-%(action)s_doc (Unix/Mac) or\n'
2568 ' gm-%(action)s_doc.bat (Windows)\n'
2569 '\n'
2570 'The command is passed the filename to %(l10n_action)s.'
2571 ) % {'action': action, 'l10n_action': l10n_action},
2572 _('Processing document part: %s') % l10n_action
2573 )
2574 else:
2575 if action == 'mail':
2576 curr_pat = gmPerson.gmCurrentPatient()
2577 emr = curr_pat.emr
2578 emr.add_clin_narrative (
2579 soap_cat = None,
2580 note = _('document part handed over to email program: %s') % self.__curr_node_data.format(single_line = True),
2581 episode = self.__curr_node_data['pk_episode']
2582 )
2583
2585 self.__process_part(action = 'print', l10n_action = _('print'))
2586
2588 self.__process_part(action = 'fax', l10n_action = _('fax'))
2589
2591 self.__process_part(action = 'mail', l10n_action = _('mail'))
2592
2594 """Save document part into directory."""
2595
2596 dlg = wx.DirDialog (
2597 parent = self,
2598 message = _('Save document part to directory ...'),
2599 defaultPath = os.path.expanduser(os.path.join('~', 'gnumed')),
2600 style = wx.DD_DEFAULT_STYLE
2601 )
2602 result = dlg.ShowModal()
2603 dirname = dlg.GetPath()
2604 dlg.Destroy()
2605
2606 if result != wx.ID_OK:
2607 return True
2608
2609 wx.BeginBusyCursor()
2610
2611 pat = gmPerson.gmCurrentPatient()
2612 fname = self.__curr_node_data.get_useful_filename (
2613 patient = pat,
2614 make_unique = True,
2615 directory = dirname
2616 )
2617
2618 cfg = gmCfg.cCfgSQL()
2619
2620
2621 chunksize = int(cfg.get2 (
2622 option = "horstspace.blob_export_chunk_size",
2623 workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace,
2624 bias = 'workplace',
2625 default = default_chunksize
2626 ))
2627
2628 fname = self.__curr_node_data.save_to_file (
2629 aChunkSize = chunksize,
2630 filename = fname,
2631 target_mime = None
2632 )
2633
2634 wx.EndBusyCursor()
2635
2636 gmDispatcher.send(signal = 'statustext', msg = _('Successfully saved document part as [%s].') % fname)
2637
2638 return True
2639
2640
2641
2642
2652
2656
2658
2659 gmHooks.run_hook_script(hook = 'before_%s_doc' % action)
2660
2661 wx.BeginBusyCursor()
2662
2663
2664 found, external_cmd = gmShellAPI.detect_external_binary('gm-%s_doc' % action)
2665 if not found:
2666 found, external_cmd = gmShellAPI.detect_external_binary('gm-%s_doc.bat' % action)
2667 if not found:
2668 _log.error('neither of gm-%s_doc or gm-%s_doc.bat found', action, action)
2669 wx.EndBusyCursor()
2670 gmGuiHelpers.gm_show_error (
2671 _('Cannot %(l10n_action)s document - %(l10n_action)s command not found.\n'
2672 '\n'
2673 'Either of gm-%(action)s_doc or gm-%(action)s_doc.bat\n'
2674 'must be in the execution path. The command will\n'
2675 'be passed a list of filenames to %(l10n_action)s.'
2676 ) % {'action': action, 'l10n_action': l10n_action},
2677 _('Processing document: %s') % l10n_action
2678 )
2679 return
2680
2681 cfg = gmCfg.cCfgSQL()
2682
2683
2684 chunksize = int(cfg.get2 (
2685 option = "horstspace.blob_export_chunk_size",
2686 workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace,
2687 bias = 'workplace',
2688 default = default_chunksize
2689 ))
2690
2691 part_files = self.__curr_node_data.save_parts_to_files(chunksize = chunksize)
2692
2693 if os.name == 'nt':
2694 blocking = True
2695 else:
2696 blocking = False
2697
2698 if action == 'print':
2699 cmd = '%s %s %s' % (
2700 external_cmd,
2701 'generic_document',
2702 ' '.join(part_files)
2703 )
2704 else:
2705 cmd = external_cmd + ' ' + ' '.join(part_files)
2706 success = gmShellAPI.run_command_in_shell (
2707 command = cmd,
2708 blocking = blocking
2709 )
2710
2711 wx.EndBusyCursor()
2712
2713 if not success:
2714 _log.error('%s command failed: [%s]', action, cmd)
2715 gmGuiHelpers.gm_show_error (
2716 _('Cannot %(l10n_action)s document - %(l10n_action)s command failed.\n'
2717 '\n'
2718 'You may need to check and fix either of\n'
2719 ' gm-%(action)s_doc (Unix/Mac) or\n'
2720 ' gm-%(action)s_doc.bat (Windows)\n'
2721 '\n'
2722 'The command is passed a list of filenames to %(l10n_action)s.'
2723 ) % {'action': action, 'l10n_action': l10n_action},
2724 _('Processing document: %s') % l10n_action
2725 )
2726
2727
2729 self.__process_doc(action = 'print', l10n_action = _('print'))
2730
2731
2733 self.__process_doc(action = 'fax', l10n_action = _('fax'))
2734
2735
2737 self.__process_doc(action = 'mail', l10n_action = _('mail'))
2738
2739
2741 dlg = wx.FileDialog (
2742 parent = self,
2743 message = _('Choose a file'),
2744 defaultDir = os.path.expanduser(os.path.join('~', 'gnumed')),
2745 defaultFile = '',
2746 wildcard = "%s (*)|*|PNGs (*.png)|*.png|PDFs (*.pdf)|*.pdf|TIFFs (*.tif)|*.tif|JPEGs (*.jpg)|*.jpg|%s (*.*)|*.*" % (_('all files'), _('all files (Win)')),
2747 style = wx.FD_OPEN | wx.FD_FILE_MUST_EXIST | wx.FD_MULTIPLE
2748 )
2749 result = dlg.ShowModal()
2750 if result != wx.ID_CANCEL:
2751 self.__curr_node_data.add_parts_from_files(files = dlg.GetPaths(), reviewer = gmStaff.gmCurrentProvider()['pk_staff'])
2752 dlg.Destroy()
2753
2754
2769
2771
2772 gmHooks.run_hook_script(hook = 'before_external_doc_access')
2773
2774 wx.BeginBusyCursor()
2775
2776
2777 found, external_cmd = gmShellAPI.detect_external_binary('gm_access_external_doc.sh')
2778 if not found:
2779 found, external_cmd = gmShellAPI.detect_external_binary('gm_access_external_doc.bat')
2780 if not found:
2781 _log.error('neither of gm_access_external_doc.sh or .bat found')
2782 wx.EndBusyCursor()
2783 gmGuiHelpers.gm_show_error (
2784 _('Cannot access external document - access command not found.\n'
2785 '\n'
2786 'Either of gm_access_external_doc.sh or *.bat must be\n'
2787 'in the execution path. The command will be passed the\n'
2788 'document type and the reference URL for processing.'
2789 ),
2790 _('Accessing external document')
2791 )
2792 return
2793
2794 cmd = '%s "%s" "%s"' % (external_cmd, self.__curr_node_data['type'], self.__curr_node_data['ext_ref'])
2795 if os.name == 'nt':
2796 blocking = True
2797 else:
2798 blocking = False
2799 success = gmShellAPI.run_command_in_shell (
2800 command = cmd,
2801 blocking = blocking
2802 )
2803
2804 wx.EndBusyCursor()
2805
2806 if not success:
2807 _log.error('External access command failed: [%s]', cmd)
2808 gmGuiHelpers.gm_show_error (
2809 _('Cannot access external document - access command failed.\n'
2810 '\n'
2811 'You may need to check and fix either of\n'
2812 ' gm_access_external_doc.sh (Unix/Mac) or\n'
2813 ' gm_access_external_doc.bat (Windows)\n'
2814 '\n'
2815 'The command is passed the document type and the\n'
2816 'external reference URL on the command line.'
2817 ),
2818 _('Accessing external document')
2819 )
2820
2822 """Save document into directory.
2823
2824 - one file per object
2825 - into subdirectory named after patient
2826 """
2827 pat = gmPerson.gmCurrentPatient()
2828 def_dir = os.path.expanduser(os.path.join('~', 'gnumed', pat.subdir_name))
2829 gmTools.mkdir(def_dir)
2830
2831 dlg = wx.DirDialog (
2832 parent = self,
2833 message = _('Save document into directory ...'),
2834 defaultPath = def_dir,
2835 style = wx.DD_DEFAULT_STYLE
2836 )
2837 result = dlg.ShowModal()
2838 dirname = dlg.GetPath()
2839 dlg.Destroy()
2840
2841 if result != wx.ID_OK:
2842 return True
2843
2844 wx.BeginBusyCursor()
2845
2846 cfg = gmCfg.cCfgSQL()
2847
2848
2849 chunksize = int(cfg.get2 (
2850 option = "horstspace.blob_export_chunk_size",
2851 workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace,
2852 bias = 'workplace',
2853 default = default_chunksize
2854 ))
2855
2856 fnames = self.__curr_node_data.save_parts_to_files(export_dir = dirname, chunksize = chunksize)
2857
2858 wx.EndBusyCursor()
2859
2860 gmDispatcher.send(signal='statustext', msg=_('Successfully saved %s parts into the directory [%s].') % (len(fnames), dirname))
2861
2862 return True
2863
2864
2867
2868
2879
2880
2881
2882
2883
2884 from Gnumed.wxGladeWidgets.wxgPACSPluginPnl import wxgPACSPluginPnl
2885
2886 -class cPACSPluginPnl(wxgPACSPluginPnl, gmRegetMixin.cRegetOnPaintMixin):
2887
2898
2899
2900
2901
2903 self._LCTRL_studies.set_columns(columns = [_('Date'), _('Description'), _('Organization'), _('Referrals')])
2904 self._LCTRL_studies.select_callback = self._on_studies_list_item_selected
2905 self._LCTRL_studies.deselect_callback = self._on_studies_list_item_deselected
2906
2907 self._LCTRL_series.set_columns(columns = [_('Time'), _('Method'), _('Body part'), _('Description')])
2908 self._LCTRL_series.select_callback = self._on_series_list_item_selected
2909 self._LCTRL_series.deselect_callback = self._on_series_list_item_deselected
2910
2911 self._LCTRL_details.set_columns(columns = [_('DICOM field'), _('Value')])
2912 self._LCTRL_details.set_column_widths()
2913
2914 self._BMP_preview.SetBitmap(wx.Bitmap.FromRGBA(50,50, red=0, green=0, blue=0, alpha = wx.ALPHA_TRANSPARENT))
2915
2916
2983
2984
2986 self._LBL_patient_identification.SetLabel('')
2987 self._LCTRL_studies.set_string_items(items = [])
2988 self._LCTRL_series.set_string_items(items = [])
2989 self.__refresh_image()
2990 self.__refresh_details()
2991
2992
2994 self._LBL_PACS_identification.SetLabel(_('<not connected>'))
2995
2996
2998 self.__reset_server_identification()
2999 self.__reset_patient_data()
3000 self.__set_button_states()
3001
3002
3004
3005 self.__pacs = None
3006 self.__orthanc_patient = None
3007 self.__set_button_states()
3008 self.__reset_server_identification()
3009
3010 host = self._TCTRL_host.Value.strip()
3011 port = self._TCTRL_port.Value.strip()[:6]
3012 if port == '':
3013 self._LBL_PACS_identification.SetLabel(_('Cannot connect without port (try 8042).'))
3014 return False
3015 if len(port) < 4:
3016 return False
3017 try:
3018 int(port)
3019 except ValueError:
3020 self._LBL_PACS_identification.SetLabel(_('Invalid port (try 8042).'))
3021 return False
3022
3023 user = self._TCTRL_user.Value
3024 if user == '':
3025 user = None
3026 self._LBL_PACS_identification.SetLabel(_('Connect to [%s] @ port %s as "%s".') % (host, port, user))
3027 password = self._TCTRL_password.Value
3028 if password == '':
3029 password = None
3030
3031 pacs = gmDICOM.cOrthancServer()
3032 if not pacs.connect(host = host, port = port, user = user, password = password):
3033 self._LBL_PACS_identification.SetLabel(_('Cannot connect to PACS.'))
3034 _log.error('error connecting to server: %s', pacs.connect_error)
3035 return False
3036
3037
3038 self._LBL_PACS_identification.SetLabel(_('PACS: Orthanc "%s" (AET "%s", Version %s, DB v%s)') % (
3039 pacs.server_identification['Name'],
3040 pacs.server_identification['DicomAet'],
3041 pacs.server_identification['Version'],
3042
3043 pacs.server_identification['DatabaseVersion']
3044 ))
3045
3046 self.__pacs = pacs
3047 self.__set_button_states()
3048 return True
3049
3050
3052
3053 self.__orthanc_patient = None
3054
3055 if not self.__patient.connected:
3056 self.__reset_patient_data()
3057 self.__set_button_states()
3058 return True
3059
3060 if not self.__connect():
3061 return False
3062
3063 tt_lines = [_('Known PACS IDs:')]
3064 for pacs_id in self.__patient.suggest_external_ids(target = 'PACS'):
3065 tt_lines.append(' ' + _('generic: %s') % pacs_id)
3066 for pacs_id in self.__patient.get_external_ids(id_type = 'PACS', issuer = self.__pacs.as_external_id_issuer):
3067 tt_lines.append(' ' + _('stored: "%(value)s" @ [%(issuer)s]') % pacs_id)
3068 tt_lines.append('')
3069 tt_lines.append(_('Patients found in PACS:'))
3070
3071 info_lines = []
3072
3073 matching_pats = self.__pacs.get_matching_patients(person = self.__patient)
3074 if len(matching_pats) == 0:
3075 info_lines.append(_('PACS: no patients with matching IDs found'))
3076 no_of_studies = 0
3077 for pat in matching_pats:
3078 info_lines.append('"%s" %s "%s (%s) %s"' % (
3079 pat['MainDicomTags']['PatientID'],
3080 gmTools.u_arrow2right,
3081 gmTools.coalesce(pat['MainDicomTags']['PatientName'], '?'),
3082 gmTools.coalesce(pat['MainDicomTags']['PatientSex'], '?'),
3083 gmTools.coalesce(pat['MainDicomTags']['PatientBirthDate'], '?')
3084 ))
3085 no_of_studies += len(pat['Studies'])
3086 tt_lines.append('%s [#%s]' % (
3087 gmTools.format_dict_like (
3088 pat['MainDicomTags'],
3089 relevant_keys = ['PatientName', 'PatientSex', 'PatientBirthDate', 'PatientID'],
3090 template = ' %(PatientID)s = %(PatientName)s (%(PatientSex)s) %(PatientBirthDate)s',
3091 missing_key_template = '?'
3092 ),
3093 pat['ID']
3094 ))
3095 if len(matching_pats) > 1:
3096 info_lines.append(_('PACS: more than one patient with matching IDs found, carefully check studies'))
3097 self._LBL_patient_identification.SetLabel('\n'.join(info_lines))
3098 tt_lines.append('')
3099 tt_lines.append(_('Studies found: %s') % no_of_studies)
3100 self._LBL_patient_identification.SetToolTip('\n'.join(tt_lines))
3101
3102
3103 study_list_items = []
3104 study_list_data = []
3105 if len(matching_pats) > 0:
3106
3107 self.__orthanc_patient = matching_pats[0]
3108 for pat in self.__pacs.get_studies_list_by_orthanc_patient_list(orthanc_patients = matching_pats):
3109 for study in pat['studies']:
3110 docs = []
3111 if study['referring_doc'] is not None:
3112 docs.append(study['referring_doc'])
3113 if study['requesting_doc'] is not None:
3114 if study['requesting_doc'] not in docs:
3115 docs.append(study['requesting_doc'])
3116 if study['operator_name'] is not None:
3117 if study['operator_name'] not in docs:
3118 docs.append(study['operator_name'])
3119 study_list_items.append( [
3120 '%s-%s-%s' % (
3121 study['date'][:4],
3122 study['date'][4:6],
3123 study['date'][6:8]
3124 ),
3125 _('%s series%s') % (
3126 len(study['series']),
3127 gmTools.coalesce(study['description'], '', ': %s')
3128 ),
3129 gmTools.coalesce(study['radiology_org'], ''),
3130 gmTools.u_arrow2right.join(docs)
3131 ] )
3132 study_list_data.append(study)
3133
3134 self._LCTRL_studies.set_string_items(items = study_list_items)
3135 self._LCTRL_studies.set_data(data = study_list_data)
3136 self._LCTRL_studies.SortListItems(0, 0)
3137 self._LCTRL_studies.set_column_widths()
3138
3139 self.__refresh_image()
3140 self.__refresh_details()
3141 self.__set_button_states()
3142
3143 self.Layout()
3144 return True
3145
3146
3180
3181
3183
3184 self.__image_data = None
3185 self._LBL_image.Label = _('Image')
3186 self._BMP_preview.SetBitmap(wx.Bitmap.FromRGBA(50,50, red=0, green=0, blue=0, alpha = wx.ALPHA_TRANSPARENT))
3187
3188 if idx is None:
3189 return
3190 if self.__pacs is None:
3191 return
3192 series = self._LCTRL_series.get_selected_item_data(only_one = True)
3193 if series is None:
3194 return
3195 if idx > len(series['instances']) - 1:
3196 raise ValueError('trying to go beyond instances in series: %s of %s', idx, len(series['instances']))
3197
3198
3199 uuid = series['instances'][idx]
3200 img_file = self.__pacs.get_instance_preview(instance_id = uuid)
3201
3202 wx_bmp = gmGuiHelpers.file2scaled_image(filename = img_file, height = 100)
3203
3204 if wx_bmp is None:
3205 _log.error('cannot load DICOM instance from PACS: %s', uuid)
3206 else:
3207 self.__image_data = {'idx': idx, 'uuid': uuid}
3208 self._BMP_preview.SetBitmap(wx_bmp)
3209 self._LBL_image.Label = _('Image %s/%s') % (idx+1, len(series['instances']))
3210
3211 if idx == 0:
3212 self._BTN_previous_image.Disable()
3213 else:
3214 self._BTN_previous_image.Enable()
3215 if idx == len(series['instances']) - 1:
3216 self._BTN_next_image.Disable()
3217 else:
3218 self._BTN_next_image.Enable()
3219
3220
3221
3222
3224 if not self.__patient.connected:
3225 self.__reset_ui_content()
3226 return True
3227
3228 if not self.__refresh_patient_data():
3229 return False
3230
3231 return True
3232
3233
3234
3235
3237
3238 gmDispatcher.connect(signal = 'pre_patient_unselection', receiver = self._on_pre_patient_unselection)
3239 gmDispatcher.connect(signal = 'post_patient_selection', receiver = self._on_post_patient_selection)
3240
3241
3242 gmDispatcher.connect(signal = 'gm_table_mod', receiver = self._on_database_signal)
3243
3244
3246
3247
3248
3249 self.__reset_patient_data()
3250
3251
3253 self._schedule_data_reget()
3254
3255
3257
3258 if not self.__patient.connected:
3259
3260
3261 return True
3262
3263 if kwds['pk_identity'] != self.__patient.ID:
3264 return True
3265
3266 if kwds['table'] == 'dem.lnk_identity2ext_id':
3267 self._schedule_data_reget()
3268 return True
3269
3270 return True
3271
3272
3273
3274
3276
3277 event.Skip()
3278 if self.__pacs is None:
3279 return
3280
3281 study_data = self._LCTRL_studies.get_selected_item_data(only_one = True)
3282 if study_data is None:
3283 return
3284
3285 series = self._LCTRL_series.get_selected_item_data(only_one = True)
3286 if series is None:
3287 self.__set_button_states()
3288 return
3289
3290 if len(series['instances']) == 0:
3291 self.__refresh_image()
3292 self.__refresh_details()
3293 self.__set_button_states()
3294 return
3295
3296
3297 self.__refresh_image(0)
3298 self.__refresh_details()
3299 self.__set_button_states()
3300 self._BTN_previous_image.Disable()
3301
3302 self.Layout()
3303
3304
3306 event.Skip()
3307
3308 self.__refresh_image()
3309 self.__refresh_details()
3310 self.__set_button_states()
3311
3312
3314 event.Skip()
3315 if self.__pacs is None:
3316 return
3317
3318 study_data = self._LCTRL_studies.get_selected_item_data(only_one = True)
3319 if study_data is None:
3320 self.__set_button_states()
3321 return
3322
3323 series_list_items = []
3324 series_list_data = []
3325 for series in study_data['series']:
3326
3327 series_time = ''
3328 if series['time'] is None:
3329 series['time'] = study_data['time']
3330 series_time = '%s:%s:%s' % (
3331 series['time'][:2],
3332 series['time'][2:4],
3333 series['time'][4:6]
3334 )
3335
3336 series_desc_parts = []
3337 if series['description'] is not None:
3338 if series['protocol'] is None:
3339 series_desc_parts.append(series['description'].strip())
3340 else:
3341 if series['description'].strip() not in series['protocol'].strip():
3342 series_desc_parts.append(series['description'].strip())
3343 if series['protocol'] is not None:
3344 series_desc_parts.append('[%s]' % series['protocol'].strip())
3345 if series['performed_procedure_step_description'] is not None:
3346 series_desc_parts.append(series['performed_procedure_step_description'].strip())
3347 if series['acquisition_device_processing_description'] is not None:
3348 series_desc_parts.append(series['acquisition_device_processing_description'].strip())
3349 series_desc = ' / '.join(series_desc_parts)
3350 if len(series_desc) > 0:
3351 series_desc = ': ' + series_desc
3352 series_desc = _('%s image(s)%s') % (len(series['instances']), series_desc)
3353
3354 series_list_items.append ([
3355 series_time,
3356 gmTools.coalesce(series['modality'], ''),
3357 gmTools.coalesce(series['body_part'], ''),
3358 series_desc
3359 ])
3360 series_list_data.append(series)
3361
3362 self._LCTRL_series.set_string_items(items = series_list_items)
3363 self._LCTRL_series.set_data(data = series_list_data)
3364 self._LCTRL_series.SortListItems(0)
3365
3366 self.__refresh_image()
3367 self.__refresh_details()
3368 self.__set_button_states()
3369
3370
3372 event.Skip()
3373
3374 self._LCTRL_series.remove_items_safely()
3375 self.__refresh_image()
3376 self.__refresh_details()
3377 self.__set_button_states()
3378
3379
3380
3381
3396
3397
3403
3404
3410
3411
3420
3421
3466
3467
3469 event.Skip()
3470 if self.__pacs is None:
3471 return
3472
3473 title = _('Working on: Orthanc "%s" (AET "%s" @ %s:%s, Version %s)') % (
3474 self.__pacs.server_identification['Name'],
3475 self.__pacs.server_identification['DicomAet'],
3476 self._TCTRL_host.Value.strip(),
3477 self._TCTRL_port.Value.strip(),
3478 self.__pacs.server_identification['Version']
3479 )
3480 dlg = cModifyOrthancContentDlg(self, -1, server = self.__pacs, title = title)
3481 dlg.ShowModal()
3482 dlg.Destroy()
3483 self._schedule_data_reget()
3484
3485
3486
3487
3493
3494
3500
3501
3515
3516
3529
3530
3543
3544
3557
3558
3580
3581
3582
3583
3617
3618
3644
3645
3646
3647
3690
3691
3723
3724
3749
3750
3751
3752
3753
3754
3755
3756
3757
3758
3759
3760
3761
3762
3763
3764
3765
3766
3767
3768
3769
3770
3771
3772
3773
3774
3775
3776
3777
3778
3779
3780
3781
3782
3783
3784
3785
3786
3787
3788
3789
3790
3791
3792
3793
3794
3795
3796
3797
3798
3799
3800
3801
3802
3803
3804
3805
3806
3807
3808
3809
3810
3811
3812
3813
3814
3815
3816
3817
3818
3819
3820
3821
3822
3823
3824
3825
3826 from Gnumed.wxGladeWidgets.wxgModifyOrthancContentDlg import wxgModifyOrthancContentDlg
3827
3828 -class cModifyOrthancContentDlg(wxgModifyOrthancContentDlg):
3829 - def __init__(self, *args, **kwds):
3830 self.__srv = kwds['server']
3831 del kwds['server']
3832 title = kwds['title']
3833 del kwds['title']
3834 wxgModifyOrthancContentDlg.__init__(self, *args, **kwds)
3835 self.SetTitle(title)
3836 self._LCTRL_patients.set_columns( [_('Patient ID'), _('Name'), _('Birth date'), _('Gender'), _('Orthanc')] )
3837
3838
3840 self._LCTRL_patients.set_string_items()
3841 search_term = self._TCTRL_search_term.Value.strip()
3842 if search_term == '':
3843 return
3844 pats = self.__srv.get_patients_by_name(name_parts = search_term.split(), fuzzy = True)
3845 if len(pats) == 0:
3846 return
3847 list_items = []
3848 list_data = []
3849 for pat in pats:
3850 mt = pat['MainDicomTags']
3851 try:
3852 gender = mt['PatientSex']
3853 except KeyError:
3854 gender = ''
3855 try:
3856 dob = mt['PatientBirthDate']
3857 except KeyError:
3858 dob = ''
3859 list_items.append([mt['PatientID'], mt['PatientName'], dob, gender, pat['ID']])
3860 list_data.append(mt['PatientID'])
3861 self._LCTRL_patients.set_string_items(list_items)
3862 self._LCTRL_patients.set_column_widths()
3863 self._LCTRL_patients.set_data(list_data)
3864
3865
3867 event.Skip()
3868 self.__refresh_patient_list()
3869
3870
3877
3878
3880 event.Skip()
3881 new_id = self._TCTRL_new_patient_id.Value.strip()
3882 if new_id == '':
3883 return
3884 pats = self._LCTRL_patients.get_selected_item_data(only_one = False)
3885 if len(pats) == 0:
3886 return
3887 really_modify = gmGuiHelpers.gm_show_question (
3888 title = _('Modifying patient ID'),
3889 question = _(
3890 'Really modify %s patient(s) to have the new patient ID\n\n'
3891 ' [%s]\n\n'
3892 'stored in the Orthanc DICOM server ?'
3893 ) % (
3894 len(pats),
3895 new_id
3896 ),
3897 cancel_button = True
3898 )
3899 if not really_modify:
3900 return
3901 all_modified = True
3902 for pat in pats:
3903 success = self.__srv.modify_patient_id(old_patient_id = pat, new_patient_id = new_id)
3904 if not success:
3905 all_modified = False
3906 self.__refresh_patient_list()
3907
3908 if not all_modified:
3909 gmGuiHelpers.gm_show_warning (
3910 aTitle = _('Modifying patient ID'),
3911 aMessage = _(
3912 'I was unable to modify all DICOM patients.\n'
3913 '\n'
3914 'Please refer to the log file.'
3915 )
3916 )
3917 return all_modified
3918
3919
3920
3922 event.Skip()
3923 dlg = wx.DirDialog (
3924 self,
3925 message = _('Select the directory from which to recursively upload DICOM files.'),
3926 defaultPath = os.path.join(gmTools.gmPaths().home_dir, 'gnumed')
3927 )
3928 choice = dlg.ShowModal()
3929 dicom_dir = dlg.GetPath()
3930 dlg.Destroy()
3931 if choice != wx.ID_OK:
3932 return True
3933 wx.BeginBusyCursor()
3934 try:
3935 uploaded, not_uploaded = self.__pacs.upload_from_directory (
3936 directory = dicom_dir,
3937 recursive = True,
3938 check_mime_type = False,
3939 ignore_other_files = True
3940 )
3941 finally:
3942 wx.EndBusyCursor()
3943 if len(not_uploaded) == 0:
3944 q = _('Delete the uploaded DICOM files now ?')
3945 else:
3946 q = _('Some files have not been uploaded.\n\nDo you want to delete those DICOM files which have been sent to the PACS successfully ?')
3947 _log.error('not uploaded:')
3948 for f in not_uploaded:
3949 _log.error(f)
3950 delete_uploaded = gmGuiHelpers.gm_show_question (
3951 title = _('Uploading DICOM files'),
3952 question = q,
3953 cancel_button = False
3954 )
3955 if not delete_uploaded:
3956 return
3957 wx.BeginBusyCursor()
3958 for f in uploaded:
3959 gmTools.remove_file(f)
3960 wx.EndBusyCursor()
3961
3962
3963
3964
3965 if __name__ == '__main__':
3966
3967 if len(sys.argv) < 2:
3968 sys.exit()
3969
3970 if sys.argv[1] != 'test':
3971 sys.exit()
3972
3973 from Gnumed.business import gmPersonSearch
3974 from Gnumed.wxpython import gmPatSearchWidgets
3975
3976
3978 app = wx.PyWidgetTester(size = (180, 20))
3979
3980 prw = cDocumentPhraseWheel(app.frame, -1)
3981 prw.set_context('pat', 12)
3982 app.frame.Show(True)
3983 app.MainLoop()
3984
3985
3986 test_document_prw()
3987