| Home | Trees | Indices | Help |
|
|---|
|
|
1 # -*- coding: utf-8 -*-
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 # 1 MB
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):
152 return save_files_as_new_document (
153 parent = parent,
154 filenames = [filename],
155 document_type = document_type,
156 unlock_patient = unlock_patient,
157 episode = episode,
158 review_as_normal = review_as_normal,
159 pk_org_unit = pk_org_unit
160 )
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 # FIXME: what to do here ? probably create dummy episode
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 # inform user
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 # remove non-temp files
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 #--------------------------------------------------------
342 if len(self._data) == 0:
343 return None
344 return gmDocuments.cDocument(aPK_obj = self.GetData())
345
346 #--------------------------------------------------------
348 if len(self._data) == 0:
349 return ''
350 return gmDocuments.cDocument(aPK_obj = self.GetData()).format(single_line = False)
351
352 #============================================================
354 """Let user select a document comment from all existing comments."""
356
357 gmPhraseWheel.cPhraseWheel.__init__(self, *args, **kwargs)
358
359 context = {
360 'ctxt_doc_type': {
361 'where_part': 'and fk_type = %(pk_doc_type)s',
362 'placeholder': 'pk_doc_type'
363 }
364 }
365
366 mp = gmMatchProvider.cMatchProvider_SQL2 (
367 queries = ["""
368 SELECT
369 data,
370 field_label,
371 list_label
372 FROM (
373 SELECT DISTINCT ON (field_label) *
374 FROM (
375 -- constrained by doc type
376 SELECT
377 comment AS data,
378 comment AS field_label,
379 comment AS list_label,
380 1 AS rank
381 FROM blobs.doc_med
382 WHERE
383 comment %(fragment_condition)s
384 %(ctxt_doc_type)s
385
386 UNION ALL
387
388 SELECT
389 comment AS data,
390 comment AS field_label,
391 comment AS list_label,
392 2 AS rank
393 FROM blobs.doc_med
394 WHERE
395 comment %(fragment_condition)s
396 ) AS q_union
397 ) AS q_distinct
398 ORDER BY rank, list_label
399 LIMIT 25"""],
400 context = context
401 )
402 mp.setThresholds(3, 5, 7)
403 mp.unset_context('pk_doc_type')
404
405 self.matcher = mp
406 self.picklist_delay = 50
407
408 self.SetToolTip(_('Enter a comment on the document.'))
409
410 #============================================================
411 # document type widgets
412 #============================================================
414
415 if parent is None:
416 parent = wx.GetApp().GetTopWindow()
417
418 dlg = cEditDocumentTypesDlg(parent = parent)
419 dlg.ShowModal()
420
421 #============================================================
422 from Gnumed.wxGladeWidgets import wxgEditDocumentTypesDlg
423
429
430 #============================================================
431 from Gnumed.wxGladeWidgets import wxgEditDocumentTypesPnl
432
434 """A panel grouping together fields to edit the list of document types."""
435
437 wxgEditDocumentTypesPnl.wxgEditDocumentTypesPnl.__init__(self, *args, **kwargs)
438 self.__init_ui()
439 self.__register_interests()
440 self.repopulate_ui()
441 #--------------------------------------------------------
443 self._LCTRL_doc_type.set_columns([_('Type'), _('Translation'), _('User defined'), _('In use')])
444 self._LCTRL_doc_type.set_column_widths()
445 #--------------------------------------------------------
448 #--------------------------------------------------------
450 self.repopulate_ui()
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 # event handlers
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 # self._LCTRL_doc_type.deselect_selected_item()
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 # document review widgets
638 #============================================================
640 if parent is None:
641 parent = wx.GetApp().GetTopWindow()
642 dlg = cReviewDocPartDlg (
643 parent = parent,
644 id = -1,
645 part = part
646 )
647 dlg.ShowModal()
648 dlg.Destroy()
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 # internal API
683 #--------------------------------------------------------
685 # FIXME: fix this
686 # associated episode (add " " to avoid popping up pick list)
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 # init my review if any
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() # FIXME: this is ugly as sin, it should be dicts, not lists
772 if len(revs) == 0:
773 return True
774 # find special reviews
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 # display them
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 # event handlers
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 # do not import globally since we might want to use
934 # this module without requiring any scanner to be available
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
964
966 wxgScanIdxPnl.wxgScanIdxPnl.__init__(self, *args, **kwds)
967 gmPlugin.cPatientChange_PluginMixin.__init__(self)
968
969 self._PhWheel_reviewer.matcher = gmPerson.cMatchProvider_Provider()
970
971 self.__init_ui_data()
972 self._PhWheel_doc_type.add_callback_on_lose_focus(self._on_doc_type_loses_focus)
973
974 # make me and listctrl file drop targets
975 dt = gmGuiHelpers.cFileDropTarget(target = self)
976 self.SetDropTarget(dt)
977 dt = gmGuiHelpers.cFileDropTarget(on_drop_callback = self._drop_target_consume_filenames)
978 self._LCTRL_doc_pages.SetDropTarget(dt)
979
980 # do not import globally since we might want to use
981 # this module without requiring any scanner to be available
982 from Gnumed.pycommon import gmScanBackend
983 self.scan_module = gmScanBackend
984
985 #--------------------------------------------------------
986 # file drop target API
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 # dive into folders dropped onto us and extract files (one level deep only)
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 # patient change plugin API
1018 #--------------------------------------------------------
1022
1023 #--------------------------------------------------------
1026
1027 #--------------------------------------------------------
1028 # internal API
1029 #--------------------------------------------------------
1031 # -----------------------------
1032 self._PhWheel_episode.SetText(value = _('other documents'), suppress_smarts = True)
1033 self._PhWheel_doc_type.SetText('')
1034 # -----------------------------
1035 # FIXME: make this configurable: either now() or last_date()
1036 fts = gmDateTime.cFuzzyTimestamp()
1037 self._PhWheel_doc_date.SetText(fts.strftime('%Y-%m-%d'), fts)
1038 self._PRW_doc_comment.SetText('')
1039 self._PhWheel_source.SetText('', None)
1040 self._RBTN_org_is_source.SetValue(1)
1041 # FIXME: should be set to patient's primary doc
1042 self._PhWheel_reviewer.selection_only = True
1043 me = gmStaff.gmCurrentProvider()
1044 self._PhWheel_reviewer.SetText (
1045 value = '%s (%s%s %s)' % (me['short_alias'], gmTools.coalesce(me['title'], ''), me['firstnames'], me['lastnames']),
1046 data = me['pk_staff']
1047 )
1048 # -----------------------------
1049 # FIXME: set from config item
1050 self._ChBOX_reviewed.SetValue(False)
1051 self._ChBOX_abnormal.Disable()
1052 self._ChBOX_abnormal.SetValue(False)
1053 self._ChBOX_relevant.Disable()
1054 self._ChBOX_relevant.SetValue(False)
1055 # -----------------------------
1056 self._TBOX_description.SetValue('')
1057 # -----------------------------
1058 # the list holding our page files
1059 self._LCTRL_doc_pages.remove_items_safely()
1060 self._LCTRL_doc_pages.set_columns([_('file'), _('path')])
1061 self._LCTRL_doc_pages.set_column_widths()
1062
1063 self._TCTRL_metadata.SetValue('')
1064
1065 self._PhWheel_doc_type.SetFocus()
1066
1067 #--------------------------------------------------------
1069 rows = gmTools.coalesce(self._LCTRL_doc_pages.string_items, [])
1070 data = gmTools.coalesce(self._LCTRL_doc_pages.data, [])
1071 rows.extend([ [gmTools.fname_from_path(f), gmTools.fname_dir(f)] for f in filenames ])
1072 data.extend([ [f, source] for f in filenames ])
1073 self._LCTRL_doc_pages.string_items = rows
1074 self._LCTRL_doc_pages.data = data
1075 self._LCTRL_doc_pages.set_column_widths()
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 # this should be optional, actually
1112 # if self._PRW_doc_comment.GetValue().strip() == '':
1113 # gmGuiHelpers.gm_show_error (
1114 # aMessage = _('No document comment supplied. Add a comment for this document.'),
1115 # aTitle = title
1116 # )
1117 # return False
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 # get_devices() not implemented for TWAIN yet
1167 # XSane has its own chooser (so does TWAIN)
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 # device_names = []
1175 # for device in devices:
1176 # device_names.append('%s (%s)' % (device[2], device[0]))
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 # FIXME: add support for actually reconfiguring
1191 return device[0]
1192
1193 #--------------------------------------------------------
1194 # event handling API
1195 #--------------------------------------------------------
1197
1198 chosen_device = self.get_device_to_use()
1199
1200 # FIXME: configure whether to use XSane or sane directly
1201 # FIXME: add support for xsane_device_settings argument
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: # no pages scanned
1223 return True
1224
1225 self.add_parts_from_files(fnames, _('captured by imaging device'))
1226 return True
1227
1228 #--------------------------------------------------------
1230 # patient file chooser
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 #--------------------------------------------------------
1249 event.Skip()
1250 clip = gmGuiHelpers.clipboard2file()
1251 if clip is None:
1252 return
1253 if clip is False:
1254 return
1255 self.add_parts_from_files([clip], _('pasted from clipboard'))
1256
1257 #--------------------------------------------------------
1259
1260 # nothing to do
1261 if self._LCTRL_doc_pages.ItemCount == 0:
1262 return
1263
1264 # only one page, show that, regardless of whether selected or not
1265 if self._LCTRL_doc_pages.ItemCount == 1:
1266 page_fnames = [ self._LCTRL_doc_pages.get_item_data(0)[0] ]
1267 else:
1268 # did user select one of multiple pages ?
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 # external reference
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 # create document
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 # - long description
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 # set reviewed status
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 #--------------------------------------------------------
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 #--------------------------------------------------------
1406 event.Skip()
1407 fname, source = self._LCTRL_doc_pages.get_item_data(item_idx = event.Index)
1408 self._TCTRL_metadata.SetValue('Retrieving details from [%s] ...' % fname)
1409 gmMimeLib.describe_file(fname, callback = self._on_update_file_description)
1410
1411 #============================================================
1413
1414 if parent is None:
1415 parent = wx.GetApp().GetTopWindow()
1416
1417 # sanity check
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 # determine database export chunk size
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 # shall we force blocking during view ?
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 # display it
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 # handle review after display
1461 # 0: never
1462 # 1: always
1463 # 2: if no review by myself exists yet
1464 # 3: if no review at all exists yet
1465 # 4: if no review by responsible reviewer
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: # always review
1473 review_document_part(parent = parent, part = part)
1474 elif review_after_display == 2: # review if no review by me exists
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):
1490
1491 pat = gmPerson.gmCurrentPatient()
1492
1493 if parent is None:
1494 parent = wx.GetApp().GetTopWindow()
1495
1496 #--------------------------------------------------------
1497 def edit(document=None):
1498 return
1499 #return edit_substance(parent = parent, substance = substance, single_entry = (substance is not None))
1500
1501 #--------------------------------------------------------
1502 def delete(document):
1503 return
1504 # if substance.is_in_use_by_patients:
1505 # gmDispatcher.send(signal = 'statustext', msg = _('Cannot delete this substance. It is in use.'), beep = True)
1506 # return False
1507 #
1508 # return gmMedication.delete_x_substance(substance = substance['pk'])
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 #new_callback = edit,
1537 #edit_callback = edit,
1538 #delete_callback = delete,
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
1546 -class cSelectablySortedDocTreePnl(wxgSelectablySortedDocTreePnl.wxgSelectablySortedDocTreePnl):
1547 """A panel with a document tree which can be sorted."""
1548
1550 wxgSelectablySortedDocTreePnl.wxgSelectablySortedDocTreePnl.__init__(self, parent, id, *args, **kwds)
1551
1552 self._LCTRL_details.set_columns(['', ''])
1553
1554 self._doc_tree.show_details_callback = self._update_details
1555
1556 #--------------------------------------------------------
1557 # inherited event handlers
1558 #--------------------------------------------------------
1560 self._doc_tree.sort_mode = 'age'
1561 self._doc_tree.SetFocus()
1562 self._rbtn_sort_by_age.SetValue(True)
1563
1564 #--------------------------------------------------------
1566 self._doc_tree.sort_mode = 'review'
1567 self._doc_tree.SetFocus()
1568 self._rbtn_sort_by_review.SetValue(True)
1569
1570 #--------------------------------------------------------
1572 self._doc_tree.sort_mode = 'episode'
1573 self._doc_tree.SetFocus()
1574 self._rbtn_sort_by_episode.SetValue(True)
1575 #--------------------------------------------------------
1577 self._doc_tree.sort_mode = 'issue'
1578 self._doc_tree.SetFocus()
1579 self._rbtn_sort_by_issue.SetValue(True)
1580 #--------------------------------------------------------
1582 self._doc_tree.sort_mode = 'type'
1583 self._doc_tree.SetFocus()
1584 self._rbtn_sort_by_type.SetValue(True)
1585 #--------------------------------------------------------
1587 self._doc_tree.sort_mode = 'org'
1588 self._doc_tree.SetFocus()
1589 self._rbtn_sort_by_org.SetValue(True)
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 #items.append([_(u'Reviewed'), gmTools.bool2subst(part['reviewed'], review, u'', u'?')])
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 #============================================================
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 #--------------------------------------------------------
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 # external API
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 # properties
1782 #--------------------------------------------------------
1785
1787 if mode is None:
1788 mode = 'age'
1789
1790 if mode == self.__sort_mode:
1791 return
1792
1793 if mode not in cDocTree._sort_modes:
1794 raise ValueError('invalid document tree sort mode [%s], valid modes: %s' % (mode, cDocTree._sort_modes))
1795
1796 self.__sort_mode = mode
1797 self.__expanded_nodes = None
1798
1799 curr_pat = gmPerson.gmCurrentPatient()
1800 if not curr_pat.connected:
1801 return
1802
1803 self._schedule_data_reget()
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 # reget-on-paint API
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 # internal helpers
1832 #--------------------------------------------------------
1834 # connect handlers
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 #wx.EVT_TREE_SEL_CHANGED (self, self.GetId(), self._on_tree_item_selected)
1840 #wx.EVT_TREE_ITEM_ACTIVATED (self, self.GetId(), self._on_activate)
1841 #wx.EVT_TREE_ITEM_RIGHT_CLICK (self, self.GetId(), self.__on_right_click)
1842 #wx.EVT_TREE_ITEM_GETTOOLTIP(self, -1, self._on_tree_item_gettooltip)
1843
1844 # wx.EVT_LEFT_DCLICK(self.tree, self.OnLeftDClick)
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 #--------------------------------------------------------
1914
1915 # document / description
1916 # self.__desc_menu = wx.Menu()
1917 # item = self.__doc_context_menu.Append(-1, _('Descriptions ...'), self.__desc_menu)
1918 # item = self.__desc_menu.Append(-1, _('Add new description'))
1919 # self.Bind(wx.EVT_MENU, self.__desc_menu, self.__add_doc_desc, item)
1920 # item = self.__desc_menu.Append(-1, _('Delete description'))
1921 # self.Bind(wx.EVT_MENU, self.__desc_menu, self.__del_doc_desc, item)
1922 # self.__desc_menu.AppendSeparator()
1923
1924 #--------------------------------------------------------
1926
1927 wx.BeginBusyCursor()
1928
1929 # clean old tree
1930 if self.root is not None:
1931 self.DeleteAllItems()
1932
1933 # init new tree
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 # read documents from database
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 # avoid recursion of GUI updating
1949 wx.EndBusyCursor()
1950 return True
1951
1952 if len(docs) == 0:
1953 wx.EndBusyCursor()
1954 return True
1955
1956 # fill new tree from document list
1957 self.SetItemHasChildren(self.root, True)
1958
1959 # add our documents as first level nodes
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 # need intermediate branch level ?
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 # this praxis ?
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 #self.SetItemData(intermediate_nodes[intermediate_label], None)
2048 #self.SetItemData(intermediate_nodes[intermediate_label], tt)
2049 # not quite right: always shows data of the _last_ document of _any_ org unit of this org
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 #self.SetItemBold(doc_node, bold = True)
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 # now add parts as child nodes
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 # restore expansion state
2103 if self.__expanded_nodes is not None:
2104 self.ExpansionState = self.__expanded_nodes
2105 # but always expand root node
2106 self.Expand(self.root)
2107 # if no expansion state available then
2108 # expand intermediate nodes as well
2109 if self.__expanded_nodes is None:
2110 # but only if there are any
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 # Windows can send bogus events so ignore that
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 # doc node
2145 if isinstance(data1, gmDocuments.cDocument):
2146 date_field = 'clin_when'
2147 #date_field = 'modified_when'
2148 if self.__sort_mode == 'age':
2149 # reverse sort by date
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 # inner sort: reverse by date
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 # inner sort: reverse by date
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 # equality
2179 if data1.has_unreviewed_parts == data2.has_unreviewed_parts:
2180 # inner sort: reverse by date
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 # inner sort: reverse by date
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 # inner sort: reverse by date
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 # reverse sort by date
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 # part node
2229 if isinstance(data1, gmDocuments.cDocumentPart):
2230 # compare sequence IDs (= "page" numbers)
2231 # FIXME: wrong order ?
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 # org unit node
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 # episode or issue node
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 # type node
2262 # else sort alphabetically by label
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 # event handlers
2279 #------------------------------------------------------------------------
2283 #------------------------------------------------------------------------
2287 #------------------------------------------------------------------------
2289 # empty out tree
2290 if self.root is not None:
2291 self.DeleteAllItems()
2292 self.root = None
2293 #------------------------------------------------------------------------
2295 # FIXME: self.__load_expansion_history_from_db (but not apply it !)
2296 self.__expanded_nodes = None
2297 self._schedule_data_reget()
2298
2299 #--------------------------------------------------------
2301 node = event.GetItem()
2302 node_data = self.GetItemData(node)
2303
2304 # pseudo root node or "type"
2305 if node_data is None:
2306 self.__show_details_callback(document = None, part = None)
2307 return
2308
2309 # document node
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 # # string nodes are labels such as episodes which may or may not have children
2342 # if isinstance(node_data, str):
2343 # self.__show_details_callback(document = None, part = None)
2344 # return
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 # exclude pseudo root node
2354 if node_data is None:
2355 return None
2356
2357 # expand/collapse documents on activation
2358 if isinstance(node_data, gmDocuments.cDocument):
2359 self.Toggle(node)
2360 return True
2361
2362 # string nodes are labels such as episodes which may or may not have children
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 # exclude pseudo root node
2380 if self.__curr_node_data is None:
2381 return None
2382
2383 # documents
2384 if isinstance(self.__curr_node_data, gmDocuments.cDocument):
2385 self.__handle_doc_context()
2386
2387 # parts
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 #--------------------------------------------------------
2396 self.__curr_node_data.set_as_active_photograph()
2397 #--------------------------------------------------------
2400 #--------------------------------------------------------
2403 #--------------------------------------------------------
2406 #--------------------------------------------------------
2408
2409 item = event.GetItem()
2410
2411 if not item.IsOk():
2412 event.SetToolTip('')
2413 return
2414
2415 data = self.GetItemData(item)
2416 # documents, parts
2417 if isinstance(data, (gmDocuments.cDocument, gmDocuments.cDocumentPart)):
2418 tt = data.format()
2419 elif isinstance(data, gmOrganization.cOrgUnit):
2420 tt = '\n'.join(data.format(with_address = True, with_org = True, with_comms = True))
2421 elif isinstance(data, dict):
2422 try:
2423 tt = data['tooltip']
2424 except KeyError:
2425 tt = ''
2426 # # explicit tooltip strings
2427 # elif isinstance(data, str):
2428 # tt = data
2429 # # other (root, "None")
2430 else:
2431 tt = ''
2432
2433 event.SetToolTip(tt)
2434
2435 #--------------------------------------------------------
2436 # internal API
2437 #--------------------------------------------------------
2439
2440 if start_node is None:
2441 start_node = self.GetRootItem()
2442
2443 # protect against empty tree where not even
2444 # a root node exists
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 #--------------------------------------------------------
2459
2460 #--------------------------------------------------------
2462 ID = None
2463 # make active patient photograph
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 # part level context menu handlers
2476 #--------------------------------------------------------
2478 """Display document part."""
2479
2480 # sanity check
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 # determine database export chunk size
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 # shall we force blocking during view ?
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 # display it
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 # handle review after display
2526 # 0: never
2527 # 1: always
2528 # 2: if no review by myself exists yet
2529 # 3: if no review at all exists yet
2530 # 4: if no review by responsible reviewer
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: # always review
2538 self.__review_part(part=part)
2539 elif review_after_display == 2: # review if no review by me exists
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 #--------------------------------------------------------
2554 dlg = cReviewDocPartDlg (
2555 parent = self,
2556 id = -1,
2557 part = part
2558 )
2559 dlg.ShowModal()
2560 dlg.Destroy()
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 # detect wrapper
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 # determine database export chunk size
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 # determine database export chunk size
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 # document level context menu handlers
2740 #--------------------------------------------------------
2742 enc = gmEncounterWidgets.select_encounters (
2743 parent = self,
2744 patient = gmPerson.gmCurrentPatient()
2745 )
2746 if not enc:
2747 return
2748 self.__curr_node_data['pk_encounter'] = enc['pk_encounter']
2749 self.__curr_node_data.save()
2750 #--------------------------------------------------------
2752 enc = gmEMRStructItems.cEncounter(aPK_obj = self.__curr_node_data['pk_encounter'])
2753 gmEncounterWidgets.edit_encounter(parent = self, encounter = enc)
2754 #--------------------------------------------------------
2756
2757 gmHooks.run_hook_script(hook = 'before_%s_doc' % action)
2758
2759 wx.BeginBusyCursor()
2760
2761 # detect wrapper
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 # determine database export chunk size
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 #--------------------------------------------------------
2854 clip = gmGuiHelpers.clipboard2file()
2855 if clip is None:
2856 return
2857 if clip is False:
2858 return
2859 gmMimeLib.call_viewer_on_file(clip, block = False)
2860 really_add = gmGuiHelpers.gm_show_question (
2861 question = _('Really add the displayed clipboard item into the document ?'),
2862 title = _('Document part from clipboard')
2863 )
2864 if not really_add:
2865 return
2866 self.__curr_node_data.add_parts_from_files(files = [clip], reviewer = gmStaff.gmCurrentProvider()['pk_staff'])
2867 #--------------------------------------------------------
2869
2870 gmHooks.run_hook_script(hook = 'before_external_doc_access')
2871
2872 wx.BeginBusyCursor()
2873
2874 # detect wrapper
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 # determine database export chunk size
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 #--------------------------------------------------------
2968 delete_it = gmGuiHelpers.gm_show_question (
2969 aMessage = _('Are you sure you want to delete the document ?'),
2970 aTitle = _('Deleting document')
2971 )
2972 if delete_it is True:
2973 curr_pat = gmPerson.gmCurrentPatient()
2974 emr = curr_pat.emr
2975 enc = emr.active_encounter
2976 gmDocuments.delete_document(document_id = self.__curr_node_data['pk_doc'], encounter_id = enc['pk_encounter'])
2977
2978 #============================================================
2979 #============================================================
2980 # PACS
2981 #============================================================
2982 from Gnumed.wxGladeWidgets.wxgPACSPluginPnl import wxgPACSPluginPnl
2983
2985
2987 wxgPACSPluginPnl.__init__(self, *args, **kwargs)
2988 gmRegetMixin.cRegetOnPaintMixin.__init__(self)
2989 self.__pacs = None
2990 self.__patient = gmPerson.gmCurrentPatient()
2991 self.__orthanc_patient = None
2992 self.__image_data = None
2993
2994 self.__init_ui()
2995 self.__register_interests()
2996
2997 #--------------------------------------------------------
2998 # internal helpers
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): #, expected_aet = 'another AET'
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 #self._LBL_PACS_identification.SetLabel(_('PACS: Orthanc "%s" (AET "%s", Version %s, API v%s, DB v%s)') % (
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 #pacs.server_identification['ApiVersion'],
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 # try to find patient
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 # get studies
3206 study_list_items = []
3207 study_list_data = []
3208 if len(matching_pats) > 0:
3209 # we don't at this point really expect more than one patient matching
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 #--------------------------------------------------------
3265
3266 self._LCTRL_details.remove_items_safely()
3267 if self.__pacs is None:
3268 return
3269
3270 # study available ?
3271 study_data = self._LCTRL_studies.get_selected_item_data(only_one = True)
3272 if study_data is None:
3273 return
3274 items = []
3275 items = [ [key, study_data['all_tags'][key]] for key in study_data['all_tags'] if ('%s' % study_data['all_tags'][key]).strip() != '' ]
3276
3277 # series available ?
3278 series = self._LCTRL_series.get_selected_item_data(only_one = True)
3279 if series is None:
3280 self._LCTRL_details.set_string_items(items = items)
3281 self._LCTRL_details.set_column_widths()
3282 return
3283 items.append([' ----- ', '--- %s ----------' % _('Series')])
3284 items.extend([ [key, series['all_tags'][key]] for key in series['all_tags'] if ('%s' % series['all_tags'][key]).strip() != '' ])
3285
3286 # image available ?
3287 if self.__image_data is None:
3288 self._LCTRL_details.set_string_items(items = items)
3289 self._LCTRL_details.set_column_widths()
3290 return
3291 tags = self.__pacs.get_instance_dicom_tags(instance_id = self.__image_data['uuid'])
3292 items.append([' ----- ', '--- %s ----------' % _('Image')])
3293 items.extend([ [key, tags[key]] for key in tags if ('%s' % tags[key]).strip() != '' ])
3294
3295 self._LCTRL_details.set_string_items(items = items)
3296 self._LCTRL_details.set_column_widths()
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 # get image
3319 uuid = series['instances'][idx]
3320 img_file = self.__pacs.get_instance_preview(instance_id = uuid)
3321 # scale
3322 wx_bmp = gmGuiHelpers.file2scaled_image(filename = img_file, height = 100)
3323 # show
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 # reget-on-paint mixin API
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 # event handling
3357 #--------------------------------------------------------
3359 # client internal signals
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 # generic database change signal
3364 gmDispatcher.connect(signal = 'gm_table_mod', receiver = self._on_database_signal)
3365
3366 #--------------------------------------------------------
3368 # only empty out here, do NOT access the patient
3369 # or else we will access the old patient while it
3370 # may not be valid anymore ...
3371 self.__reset_patient_data()
3372
3373 #--------------------------------------------------------
3376
3377 #--------------------------------------------------------
3379
3380 if not self.__patient.connected:
3381 # probably not needed:
3382 #self._schedule_data_reget()
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 # events: lists
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 # set first image
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 # events: buttons
3501 #--------------------------------------------------------
3516
3517 #--------------------------------------------------------
3523
3524 #--------------------------------------------------------
3530
3531 #--------------------------------------------------------
3540
3541 #--------------------------------------------------------
3586
3587 #--------------------------------------------------------
3604
3605 #--------------------------------------------------------
3606 # - image buttons
3607 #--------------------------------------------------------
3613
3614 #--------------------------------------------------------
3620
3621 #--------------------------------------------------------
3635
3636 #--------------------------------------------------------
3649
3650 #--------------------------------------------------------
3663
3664 #--------------------------------------------------------
3677
3678 #--------------------------------------------------------
3700
3701 #--------------------------------------------------------
3702 # - study buttons
3703 #--------------------------------------------------------
3737
3738 #--------------------------------------------------------
3764
3765 #--------------------------------------------------------
3766 # - patient buttons (= all studies)
3767 #--------------------------------------------------------
3811
3812 #--------------------------------------------------------
3844
3845 #--------------------------------------------------------
3870
3871 # #--------------------------------------------------------
3872 # # check size and confirm if huge
3873 # zip_size = os.path.getsize(filename)
3874 # if zip_size > (300 * gmTools._MB): # ~ 1/2 CD-ROM
3875 # really_export = gmGuiHelpers.gm_show_question (
3876 # title = _('Exporting DICOM studies'),
3877 # question = _('The DICOM studies are %s in compressed size.\n\nReally copy to export area ?') % gmTools.size2str(zip_size),
3878 # cancel_button = False
3879 # )
3880 # if not really_export:
3881 # wx.BeginBusyCursor()
3882 # gmTools.remove_file(filename)
3883 # wx.EndBusyCursor()
3884 # return
3885 #
3886 # # import into export area
3887 # wx.BeginBusyCursor()
3888 # self.__patient.export_area.add_file (
3889 # filename = filename,
3890 # hint = _('All DICOM studies of [%s] from Orthanc PACS "%s" (AET "%s")') % (
3891 # self.__orthanc_patient['MainDicomTags']['PatientID'],
3892 # self.__pacs.server_identification['Name'],
3893 # self.__pacs.server_identification['DicomAet']
3894 # )
3895 # )
3896 # gmTools.remove_file(filename)
3897 # wx.EndBusyCursor()
3898 #
3899 # def _on_export_study_button_pressed(self, event):
3900 # event.Skip()
3901 # if self.__pacs is None:
3902 # return
3903 #
3904 # study_data = self._LCTRL_studies.get_selected_item_data(only_one = False)
3905 # if len(study_data) == 0:
3906 # return
3907 #
3908 # wx.BeginBusyCursor()
3909 # filename = self.__pacs.get_studies_with_dicomdir(study_ids = [ s['orthanc_id'] for s in study_data ], create_zip = True)
3910 # wx.EndBusyCursor()
3911 #
3912 # if filename is False:
3913 # gmGuiHelpers.gm_show_error (
3914 # title = _('Exporting DICOM studies'),
3915 # error = _('Unable to export selected studies.')
3916 # )
3917 # return
3918 #
3919 # # check size and confirm if huge
3920 # zip_size = os.path.getsize(filename)
3921 # if zip_size > (300 * gmTools._MB): # ~ 1/2 CD-ROM
3922 # really_export = gmGuiHelpers.gm_show_question (
3923 # title = _('Exporting DICOM studies'),
3924 # question = _('The DICOM studies are %s in compressed size.\n\nReally copy to export area ?') % gmTools.size2str(zip_size),
3925 # cancel_button = False
3926 # )
3927 # if not really_export:
3928 # wx.BeginBusyCursor()
3929 # gmTools.remove_file(filename)
3930 # wx.EndBusyCursor()
3931 # return
3932 #
3933 # # import into export area
3934 # wx.BeginBusyCursor()
3935 # self.__patient.export_area.add_file (
3936 # filename = filename,
3937 # hint = _('DICOM studies of [%s] from Orthanc PACS "%s" (AET "%s")') % (
3938 # self.__orthanc_patient['MainDicomTags']['PatientID'],
3939 # self.__pacs.server_identification['Name'],
3940 # self.__pacs.server_identification['DicomAet']
3941 # )
3942 # )
3943 # gmTools.remove_file(filename)
3944 # wx.EndBusyCursor()
3945
3946 #------------------------------------------------------------
3947 from Gnumed.wxGladeWidgets.wxgModifyOrthancContentDlg import wxgModifyOrthancContentDlg
3948
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 #--------------------------------------------------------
3990
3991 #--------------------------------------------------------
3998
3999 #--------------------------------------------------------
4039
4040 #------------------------------------------------------------
4041 # outdated:
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 # main
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 #pnl = cEncounterEditAreaPnl(app.frame, -1, encounter=enc)
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
| Home | Trees | Indices | Help |
|
|---|
| Generated by Epydoc 3.0.1 on Sun Aug 19 01:55:20 2018 | http://epydoc.sourceforge.net |