| Home | Trees | Indices | Help |
|
|---|
|
|
1 """GNUmed EMR structure editors
2
3 This module contains widgets to create and edit EMR structural
4 elements (issues, enconters, episodes).
5
6 This is based on initial work and ideas by Syan <kittylitter@swiftdsl.com.au>
7 and Karsten <Karsten.Hilbert@gmx.net>.
8 """
9 #================================================================
10 __author__ = "cfmoro1976@yahoo.es, karsten.hilbert@gmx.net"
11 __license__ = "GPL v2 or later"
12
13 # stdlib
14 import sys
15 import time
16 import logging
17 import datetime as pydt
18
19
20 # 3rd party
21 import wx
22
23
24 # GNUmed
25 if __name__ == '__main__':
26 sys.path.insert(0, '../../')
27
28 from Gnumed.pycommon import gmI18N
29 from Gnumed.pycommon import gmDateTime
30
31 if __name__ == '__main__':
32 gmI18N.activate_locale()
33 gmI18N.install_domain()
34 gmDateTime.init()
35
36 from Gnumed.pycommon import gmExceptions
37 from Gnumed.pycommon import gmCfg
38 from Gnumed.pycommon import gmTools
39 from Gnumed.pycommon import gmDispatcher
40 from Gnumed.pycommon import gmMatchProvider
41
42 from Gnumed.business import gmEMRStructItems
43 from Gnumed.business import gmPraxis
44 from Gnumed.business import gmPerson
45
46 from Gnumed.wxpython import gmPhraseWheel
47 from Gnumed.wxpython import gmGuiHelpers
48 from Gnumed.wxpython import gmListWidgets
49 from Gnumed.wxpython import gmEditArea
50
51
52 _log = logging.getLogger('gm.ui')
53
54 #================================================================
55 # EMR access helper functions
56 #----------------------------------------------------------------
58 """Spin time in seconds but let wx go on."""
59 if time2spin == 0:
60 return
61 sleep_time = 0.1 # 100ms
62 total_rounds = int(time2spin / sleep_time)
63 if total_rounds < 1:
64 wx.Yield()
65 time.sleep(sleep_time)
66 return
67 rounds = 0
68 while rounds < total_rounds:
69 wx.Yield()
70 time.sleep(sleep_time)
71 rounds += 1
72
73 #================================================================
74 # episode related widgets/functions
75 #----------------------------------------------------------------
77 ea = cEpisodeEditAreaPnl(parent, -1)
78 ea.data = episode
79 ea.mode = gmTools.coalesce(episode, 'new', 'edit')
80 dlg = gmEditArea.cGenericEditAreaDlg2(parent, -1, edit_area = ea, single_entry = True)
81 dlg.SetTitle(gmTools.coalesce(episode, _('Adding a new episode'), _('Editing an episode')))
82 if dlg.ShowModal() == wx.ID_OK:
83 return True
84 return False
85
86 #----------------------------------------------------------------
88
89 pat = gmPerson.gmCurrentPatient()
90 emr = pat.emr
91
92 if parent is None:
93 parent = wx.GetApp().GetTopWindow()
94 #-----------------------------------------
95 def edit(episode=None):
96 return edit_episode(parent = parent, episode = episode)
97 #-----------------------------------------
98 def delete(episode=None):
99 if gmEMRStructItems.delete_episode(episode = episode):
100 return True
101 gmDispatcher.send (
102 signal = 'statustext',
103 msg = _('Cannot delete episode.'),
104 beep = True
105 )
106 return False
107 #-----------------------------------------
108 def manage_issues(episode=None):
109 return select_health_issues(parent = None, emr = emr)
110 #-----------------------------------------
111 def get_tooltip(data):
112 if data is None:
113 return None
114 return data.format (
115 patient = pat,
116 with_summary = True,
117 with_codes = True,
118 with_encounters = False,
119 with_documents = False,
120 with_hospital_stays = False,
121 with_procedures = False,
122 with_family_history = False,
123 with_tests = False,
124 with_vaccinations = False,
125 with_health_issue = True
126 )
127 #-----------------------------------------
128 def refresh(lctrl):
129 epis = emr.get_episodes(order_by = 'description')
130 items = [
131 [ e['description'],
132 gmTools.bool2subst(e['episode_open'], _('ongoing'), _('closed'), '<unknown>'),
133 gmDateTime.pydt_strftime(e.best_guess_clinical_start_date, '%Y %b %d'),
134 gmTools.coalesce(e['health_issue'], '')
135 ] for e in epis
136 ]
137 lctrl.set_string_items(items = items)
138 lctrl.set_data(data = epis)
139 #-----------------------------------------
140 gmListWidgets.get_choices_from_list (
141 parent = parent,
142 msg = _('\nSelect the episode you want to edit !\n'),
143 caption = _('Editing episodes ...'),
144 columns = [_('Episode'), _('Status'), _('Started'), _('Health issue')],
145 single_selection = True,
146 edit_callback = edit,
147 new_callback = edit,
148 delete_callback = delete,
149 refresh_callback = refresh,
150 list_tooltip_callback = get_tooltip,
151 left_extra_button = (_('Manage issues'), _('Manage health issues'), manage_issues)
152 )
153
154 #----------------------------------------------------------------
156
157 created_new_issue = False
158
159 try:
160 issue = gmEMRStructItems.cHealthIssue(name = episode['description'], patient = episode['pk_patient'])
161 except gmExceptions.NoSuchBusinessObjectError:
162 issue = None
163
164 if issue is None:
165 issue = emr.add_health_issue(issue_name = episode['description'])
166 created_new_issue = True
167 else:
168 # issue exists already, so ask user
169 dlg = gmGuiHelpers.c3ButtonQuestionDlg (
170 parent,
171 -1,
172 caption = _('Promoting episode to health issue'),
173 question = _(
174 'There already is a health issue\n'
175 '\n'
176 ' %s\n'
177 '\n'
178 'What do you want to do ?'
179 ) % issue['description'],
180 button_defs = [
181 {'label': _('Use existing'), 'tooltip': _('Move episode into existing health issue'), 'default': False},
182 {'label': _('Create new'), 'tooltip': _('Create a new health issue with another name'), 'default': True}
183 ]
184 )
185 use_existing = dlg.ShowModal()
186 dlg.Destroy()
187
188 if use_existing == wx.ID_CANCEL:
189 return
190
191 # user wants to create new issue with alternate name
192 if use_existing == wx.ID_NO:
193 # loop until name modified but non-empty or cancelled
194 issue_name = episode['description']
195 while issue_name == episode['description']:
196 dlg = wx.TextEntryDialog (
197 parent = parent,
198 message = _('Enter a short descriptive name for the new health issue:'),
199 caption = _('Creating a new health issue ...'),
200 defaultValue = issue_name,
201 style = wx.OK | wx.CANCEL | wx.CENTRE
202 )
203 decision = dlg.ShowModal()
204 if decision != wx.ID_OK:
205 dlg.Destroy()
206 return
207 issue_name = dlg.GetValue().strip()
208 dlg.Destroy()
209 if issue_name == '':
210 issue_name = episode['description']
211
212 issue = emr.add_health_issue(issue_name = issue_name)
213 created_new_issue = True
214
215 # eventually move the episode to the issue
216 if not move_episode_to_issue(episode = episode, target_issue = issue, save_to_backend = True):
217 # user cancelled the move so delete just-created issue
218 if created_new_issue:
219 # shouldn't fail as it is completely new
220 gmEMRStructItems.delete_health_issue(health_issue = issue)
221 return
222
223 return
224
225 #----------------------------------------------------------------
227 """Prepare changing health issue for an episode.
228
229 Checks for two-open-episodes conflict. When this
230 function succeeds, the pk_health_issue has been set
231 on the episode instance and the episode should - for
232 all practical purposes - be ready for save_payload().
233 """
234 # episode is closed: should always work
235 if not episode['episode_open']:
236 episode['pk_health_issue'] = target_issue['pk_health_issue']
237 if save_to_backend:
238 episode.save_payload()
239 return True
240
241 # un-associate: should always work, too
242 if target_issue is None:
243 episode['pk_health_issue'] = None
244 if save_to_backend:
245 episode.save_payload()
246 return True
247
248 # try closing possibly expired episode on target issue if any
249 db_cfg = gmCfg.cCfgSQL()
250 epi_ttl = int(db_cfg.get2 (
251 option = 'episode.ttl',
252 workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace,
253 bias = 'user',
254 default = 60 # 2 months
255 ))
256 if target_issue.close_expired_episode(ttl=epi_ttl) is True:
257 gmDispatcher.send(signal='statustext', msg=_('Closed episodes older than %s days on health issue [%s]') % (epi_ttl, target_issue['description']))
258 existing_epi = target_issue.get_open_episode()
259
260 # no more open episode on target issue: should work now
261 if existing_epi is None:
262 episode['pk_health_issue'] = target_issue['pk_health_issue']
263 if save_to_backend:
264 episode.save_payload()
265 return True
266
267 # don't conflict on SELF ;-)
268 if existing_epi['pk_episode'] == episode['pk_episode']:
269 episode['pk_health_issue'] = target_issue['pk_health_issue']
270 if save_to_backend:
271 episode.save_payload()
272 return True
273
274 # we got two open episodes at once, ask user
275 move_range = (episode.best_guess_clinical_start_date, episode.best_guess_clinical_end_date)
276 if move_range[1] is None:
277 move_range_end = '?'
278 else:
279 move_range_end = move_range[1].strftime('%m/%y')
280 exist_range = (existing_epi.best_guess_clinical_start_date, existing_epi.best_guess_clinical_end_date)
281 if exist_range[1] is None:
282 exist_range_end = '?'
283 else:
284 exist_range_end = exist_range[1].strftime('%m/%y')
285 question = _(
286 'You want to associate the running episode:\n\n'
287 ' "%(new_epi_name)s" (%(new_epi_start)s - %(new_epi_end)s)\n\n'
288 'with the health issue:\n\n'
289 ' "%(issue_name)s"\n\n'
290 'There already is another episode running\n'
291 'for this health issue:\n\n'
292 ' "%(old_epi_name)s" (%(old_epi_start)s - %(old_epi_end)s)\n\n'
293 'However, there can only be one running\n'
294 'episode per health issue.\n\n'
295 'Which episode do you want to close ?'
296 ) % {
297 'new_epi_name': episode['description'],
298 'new_epi_start': move_range[0].strftime('%m/%y'),
299 'new_epi_end': move_range_end,
300 'issue_name': target_issue['description'],
301 'old_epi_name': existing_epi['description'],
302 'old_epi_start': exist_range[0].strftime('%m/%y'),
303 'old_epi_end': exist_range_end
304 }
305 dlg = gmGuiHelpers.c3ButtonQuestionDlg (
306 parent = None,
307 id = -1,
308 caption = _('Resolving two-running-episodes conflict'),
309 question = question,
310 button_defs = [
311 {'label': _('old episode'), 'default': True, 'tooltip': _('close existing episode "%s"') % existing_epi['description']},
312 {'label': _('new episode'), 'default': False, 'tooltip': _('close moving (new) episode "%s"') % episode['description']}
313 ]
314 )
315 decision = dlg.ShowModal()
316
317 if decision == wx.ID_CANCEL:
318 # button 3: move cancelled by user
319 return False
320
321 elif decision == wx.ID_YES:
322 # button 1: close old episode
323 existing_epi['episode_open'] = False
324 existing_epi.save_payload()
325
326 elif decision == wx.ID_NO:
327 # button 2: close new episode
328 episode['episode_open'] = False
329
330 else:
331 raise ValueError('invalid result from c3ButtonQuestionDlg: [%s]' % decision)
332
333 episode['pk_health_issue'] = target_issue['pk_health_issue']
334 if save_to_backend:
335 episode.save_payload()
336 return True
337
338 #----------------------------------------------------------------
340
341 # FIXME: support pre-selection
342
344
345 episodes = kwargs['episodes']
346 del kwargs['episodes']
347
348 gmListWidgets.cGenericListSelectorDlg.__init__(self, *args, **kwargs)
349
350 self.SetTitle(_('Select the episodes you are interested in ...'))
351 self._LCTRL_items.set_columns([_('Episode'), _('Status'), _('Health Issue')])
352 self._LCTRL_items.set_string_items (
353 items = [
354 [ epi['description'],
355 gmTools.bool2str(epi['episode_open'], _('ongoing'), ''),
356 gmTools.coalesce(epi['health_issue'], '')
357 ]
358 for epi in episodes ]
359 )
360 self._LCTRL_items.set_column_widths()
361 self._LCTRL_items.set_data(data = episodes)
362
363 #----------------------------------------------------------------
365 """Let user select an episode *description*.
366
367 The user can select an episode description from the previously
368 used descriptions across all episodes across all patients.
369
370 Selection is done with a phrasewheel so the user can
371 type the episode name and matches will be shown. Typing
372 "*" will show the entire list of episodes.
373
374 If the user types a description not existing yet a
375 new episode description will be returned.
376 """
378
379 mp = gmMatchProvider.cMatchProvider_SQL2 (
380 queries = [
381 """
382 SELECT DISTINCT ON (description)
383 description
384 AS data,
385 description
386 AS field_label,
387 description || ' ('
388 || CASE
389 WHEN is_open IS TRUE THEN _('ongoing')
390 ELSE _('closed')
391 END
392 || ')'
393 AS list_label
394 FROM
395 clin.episode
396 WHERE
397 description %(fragment_condition)s
398 ORDER BY description
399 LIMIT 30
400 """
401 ]
402 )
403 gmPhraseWheel.cPhraseWheel.__init__(self, *args, **kwargs)
404 self.matcher = mp
405
406 #----------------------------------------------------------------
408 """Let user select an episode.
409
410 The user can select an episode from the existing episodes of a
411 patient. Selection is done with a phrasewheel so the user
412 can type the episode name and matches will be shown. Typing
413 "*" will show the entire list of episodes. Closed episodes
414 will be marked as such. If the user types an episode name not
415 in the list of existing episodes a new episode can be created
416 from it if the programmer activated that feature.
417
418 If keyword <patient_id> is set to None or left out the control
419 will listen to patient change signals and therefore act on
420 gmPerson.gmCurrentPatient() changes.
421 """
423
424 ctxt = {'ctxt_pat': {'where_part': 'and pk_patient = %(pat)s', 'placeholder': 'pat'}}
425
426 mp = gmMatchProvider.cMatchProvider_SQL2 (
427 queries = [
428 """(
429
430 SELECT
431 pk_episode
432 as data,
433 description
434 as field_label,
435 coalesce (
436 description || ' - ' || health_issue,
437 description
438 ) as list_label,
439 1 as rank
440 from
441 clin.v_pat_episodes
442 where
443 episode_open is true and
444 description %(fragment_condition)s
445 %(ctxt_pat)s
446
447 ) union all (
448
449 SELECT
450 pk_episode
451 as data,
452 description
453 as field_label,
454 coalesce (
455 description || _(' (closed)') || ' - ' || health_issue,
456 description || _(' (closed)')
457 ) as list_label,
458 2 as rank
459 from
460 clin.v_pat_episodes
461 where
462 description %(fragment_condition)s and
463 episode_open is false
464 %(ctxt_pat)s
465
466 )
467
468 order by rank, list_label
469 limit 30"""
470 ],
471 context = ctxt
472 )
473
474 try:
475 kwargs['patient_id']
476 except KeyError:
477 kwargs['patient_id'] = None
478
479 if kwargs['patient_id'] is None:
480 self.use_current_patient = True
481 self.__register_patient_change_signals()
482 pat = gmPerson.gmCurrentPatient()
483 if pat.connected:
484 mp.set_context('pat', pat.ID)
485 else:
486 self.use_current_patient = False
487 self.__patient_id = int(kwargs['patient_id'])
488 mp.set_context('pat', self.__patient_id)
489
490 del kwargs['patient_id']
491
492 gmPhraseWheel.cPhraseWheel.__init__ (
493 self,
494 *args,
495 **kwargs
496 )
497 self.matcher = mp
498 #--------------------------------------------------------
499 # external API
500 #--------------------------------------------------------
502 if self.use_current_patient:
503 return False
504 self.__patient_id = int(patient_id)
505 self.set_context('pat', self.__patient_id)
506 return True
507 #--------------------------------------------------------
509 self.__is_open_for_create_data = is_open # used (only) in _create_data()
510 return gmPhraseWheel.cPhraseWheel.GetData(self, can_create = can_create, as_instance = as_instance)
511 #--------------------------------------------------------
513
514 epi_name = self.GetValue().strip()
515 if epi_name == '':
516 gmDispatcher.send(signal = 'statustext', msg = _('Cannot create episode without name.'), beep = True)
517 _log.debug('cannot create episode without name')
518 return
519
520 if self.use_current_patient:
521 pat = gmPerson.gmCurrentPatient()
522 else:
523 pat = gmPerson.cPatient(aPK_obj = self.__patient_id)
524
525 emr = pat.emr
526 epi = emr.add_episode(episode_name = epi_name, is_open = self.__is_open_for_create_data)
527 if epi is None:
528 self.data = {}
529 else:
530 self.SetText (
531 value = epi_name,
532 data = epi['pk_episode']
533 )
534 #--------------------------------------------------------
537 #--------------------------------------------------------
538 # internal API
539 #--------------------------------------------------------
541 gmDispatcher.connect(self._pre_patient_unselection, 'pre_patient_unselection')
542 gmDispatcher.connect(self._post_patient_selection, 'post_patient_selection')
543 #--------------------------------------------------------
549 #--------------------------------------------------------
551 if self.use_current_patient:
552 patient = gmPerson.gmCurrentPatient()
553 self.set_context('pat', patient.ID)
554 return True
555
556 #----------------------------------------------------------------
557 from Gnumed.wxGladeWidgets import wxgEpisodeEditAreaPnl
558
559 -class cEpisodeEditAreaPnl(gmEditArea.cGenericEditAreaMixin, wxgEpisodeEditAreaPnl.wxgEpisodeEditAreaPnl):
560
562
563 try:
564 episode = kwargs['episode']
565 del kwargs['episode']
566 except KeyError:
567 episode = None
568
569 wxgEpisodeEditAreaPnl.wxgEpisodeEditAreaPnl.__init__(self, *args, **kwargs)
570 gmEditArea.cGenericEditAreaMixin.__init__(self)
571
572 self.data = episode
573
574 #----------------------------------------------------------------
575 # generic Edit Area mixin API
576 #----------------------------------------------------------------
578
579 errors = False
580
581 if len(self._PRW_description.GetValue().strip()) == 0:
582 errors = True
583 self._PRW_description.display_as_valid(False)
584 self._PRW_description.SetFocus()
585 else:
586 self._PRW_description.display_as_valid(True)
587 self._PRW_description.Refresh()
588
589 return not errors
590
591 #----------------------------------------------------------------
593
594 pat = gmPerson.gmCurrentPatient()
595 emr = pat.emr
596
597 epi = emr.add_episode(episode_name = self._PRW_description.GetValue().strip())
598 epi['summary'] = self._TCTRL_status.GetValue().strip()
599 epi['episode_open'] = not self._CHBOX_closed.IsChecked()
600 epi['diagnostic_certainty_classification'] = self._PRW_certainty.GetData()
601
602 issue_name = self._PRW_issue.GetValue().strip()
603 if len(issue_name) != 0:
604 epi['pk_health_issue'] = self._PRW_issue.GetData(can_create = True)
605 issue = gmEMRStructItems.cHealthIssue(aPK_obj = epi['pk_health_issue'])
606
607 if not move_episode_to_issue(episode = epi, target_issue = issue, save_to_backend = False):
608 gmDispatcher.send (
609 signal = 'statustext',
610 msg = _('Cannot attach episode [%s] to health issue [%s] because it already has a running episode.') % (
611 epi['description'],
612 issue['description']
613 )
614 )
615 gmEMRStructItems.delete_episode(episode = epi)
616 return False
617
618 epi.save()
619
620 epi.generic_codes = [ c['data'] for c in self._PRW_codes.GetData() ]
621
622 self.data = epi
623 return True
624
625 #----------------------------------------------------------------
627
628 self.data['description'] = self._PRW_description.GetValue().strip()
629 self.data['summary'] = self._TCTRL_status.GetValue().strip()
630 self.data['episode_open'] = not self._CHBOX_closed.IsChecked()
631 self.data['diagnostic_certainty_classification'] = self._PRW_certainty.GetData()
632
633 issue_name = self._PRW_issue.GetValue().strip()
634 if len(issue_name) == 0:
635 self.data['pk_health_issue'] = None
636 else:
637 self.data['pk_health_issue'] = self._PRW_issue.GetData(can_create = True)
638 issue = gmEMRStructItems.cHealthIssue(aPK_obj = self.data['pk_health_issue'])
639
640 if not move_episode_to_issue(episode = self.data, target_issue = issue, save_to_backend = False):
641 gmDispatcher.send (
642 signal = 'statustext',
643 msg = _('Cannot attach episode [%s] to health issue [%s] because it already has a running episode.') % (
644 self.data['description'],
645 issue['description']
646 )
647 )
648 return False
649
650 self.data.save()
651 self.data.generic_codes = [ c['data'] for c in self._PRW_codes.GetData() ]
652
653 return True
654
655 #----------------------------------------------------------------
657 if self.data is None:
658 ident = gmPerson.gmCurrentPatient()
659 else:
660 ident = gmPerson.cPerson(aPK_obj = self.data['pk_patient'])
661 self._TCTRL_patient.SetValue(ident.get_description_gender())
662 self._PRW_issue.SetText()
663 self._PRW_description.SetText()
664 self._TCTRL_status.SetValue('')
665 self._PRW_certainty.SetText()
666 self._CHBOX_closed.SetValue(False)
667 self._PRW_codes.SetText()
668
669 self._PRW_issue.SetFocus()
670
671 #----------------------------------------------------------------
673 ident = gmPerson.cPerson(aPK_obj = self.data['pk_patient'])
674 self._TCTRL_patient.SetValue(ident.get_description_gender())
675
676 if self.data['pk_health_issue'] is not None:
677 self._PRW_issue.SetText (
678 self.data['health_issue'],
679 data = self.data['pk_health_issue']
680 )
681
682 self._PRW_description.SetText (
683 self.data['description'],
684 data = self.data['description']
685 )
686
687 self._TCTRL_status.SetValue(gmTools.coalesce(self.data['summary'], ''))
688
689 if self.data['diagnostic_certainty_classification'] is not None:
690 self._PRW_certainty.SetData(data = self.data['diagnostic_certainty_classification'])
691
692 self._CHBOX_closed.SetValue(not self.data['episode_open'])
693
694 val, data = self._PRW_codes.generic_linked_codes2item_dict(self.data.generic_codes)
695 self._PRW_codes.SetText(val, data)
696
697 if self.data['pk_health_issue'] is None:
698 self._PRW_issue.SetFocus()
699 else:
700 self._PRW_description.SetFocus()
701
702 #----------------------------------------------------------------
705
706 #================================================================
707 # health issue related widgets/functions
708 #----------------------------------------------------------------
710 ea = cHealthIssueEditAreaPnl(parent, -1)
711 ea.data = issue
712 ea.mode = gmTools.coalesce(issue, 'new', 'edit')
713 dlg = gmEditArea.cGenericEditAreaDlg2(parent, -1, edit_area = ea, single_entry = (issue is not None))
714 dlg.SetTitle(gmTools.coalesce(issue, _('Adding a new health issue'), _('Editing a health issue')))
715 if dlg.ShowModal() == wx.ID_OK:
716 dlg.Destroy()
717 return True
718 dlg.Destroy()
719 return False
720 #----------------------------------------------------------------
722
723 if parent is None:
724 parent = wx.GetApp().GetTopWindow()
725 #-----------------------------------------
726 def edit(issue=None):
727 return edit_health_issue(parent = parent, issue = issue)
728 #-----------------------------------------
729 def delete(issue=None):
730 if gmEMRStructItems.delete_health_issue(health_issue = issue):
731 return True
732 gmDispatcher.send (
733 signal = 'statustext',
734 msg = _('Cannot delete health issue.'),
735 beep = True
736 )
737 return False
738 #-----------------------------------------
739 def get_tooltip(data):
740 if data is None:
741 return None
742 patient = gmPerson.cPatient(data['pk_patient'])
743 return data.format (
744 patient = patient,
745 with_summary = True,
746 with_codes = True,
747 with_episodes = True,
748 with_encounters = True,
749 with_medications = False,
750 with_hospital_stays = False,
751 with_procedures = False,
752 with_family_history = False,
753 with_documents = False,
754 with_tests = False,
755 with_vaccinations = False
756 )
757 #-----------------------------------------
758 def refresh(lctrl):
759 issues = emr.get_health_issues()
760 items = [
761 [
762 gmTools.bool2subst(i['is_confidential'], _('CONFIDENTIAL'), '', ''),
763 i['description'],
764 gmTools.bool2subst(i['clinically_relevant'], _('relevant'), '', ''),
765 gmTools.bool2subst(i['is_active'], _('active'), '', ''),
766 gmTools.bool2subst(i['is_cause_of_death'], _('fatal'), '', '')
767 ] for i in issues
768 ]
769 lctrl.set_string_items(items = items)
770 lctrl.set_data(data = issues)
771 #-----------------------------------------
772 return gmListWidgets.get_choices_from_list (
773 parent = parent,
774 msg = _('\nSelect the health issues !\n'),
775 caption = _('Showing health issues ...'),
776 columns = ['', _('Health issue'), '', '', ''],
777 single_selection = False,
778 edit_callback = edit,
779 new_callback = edit,
780 delete_callback = delete,
781 refresh_callback = refresh
782 )
783 #----------------------------------------------------------------
785
786 # FIXME: support pre-selection
787
789
790 issues = kwargs['issues']
791 del kwargs['issues']
792
793 gmListWidgets.cGenericListSelectorDlg.__init__(self, *args, **kwargs)
794
795 self.SetTitle(_('Select the health issues you are interested in ...'))
796 self._LCTRL_items.set_columns(['', _('Health Issue'), '', '', ''])
797
798 for issue in issues:
799 if issue['is_confidential']:
800 row_num = self._LCTRL_items.InsertItem(sys.maxsize, label = _('confidential'))
801 self._LCTRL_items.SetItemTextColour(row_num, col=wx.Colour('RED'))
802 else:
803 row_num = self._LCTRL_items.InsertItem(sys.maxsize, label = '')
804
805 self._LCTRL_items.SetItem(index = row_num, column = 1, label = issue['description'])
806 if issue['clinically_relevant']:
807 self._LCTRL_items.SetItem(index = row_num, column = 2, label = _('relevant'))
808 if issue['is_active']:
809 self._LCTRL_items.SetItem(index = row_num, column = 3, label = _('active'))
810 if issue['is_cause_of_death']:
811 self._LCTRL_items.SetItem(index = row_num, column = 4, label = _('fatal'))
812
813 self._LCTRL_items.set_column_widths()
814 self._LCTRL_items.set_data(data = issues)
815
816 #----------------------------------------------------------------
818 """Let the user select a health issue.
819
820 The user can select a health issue from the existing issues
821 of a patient. Selection is done with a phrasewheel so the user
822 can type the issue name and matches will be shown. Typing
823 "*" will show the entire list of issues. Inactive issues
824 will be marked as such. If the user types an issue name not
825 in the list of existing issues a new issue can be created
826 from it if the programmer activated that feature.
827
828 If keyword <patient_id> is set to None or left out the control
829 will listen to patient change signals and therefore act on
830 gmPerson.gmCurrentPatient() changes.
831 """
833
834 ctxt = {'ctxt_pat': {'where_part': 'pk_patient=%(pat)s', 'placeholder': 'pat'}}
835
836 mp = gmMatchProvider.cMatchProvider_SQL2 (
837 # FIXME: consider clin.health_issue.clinically_relevant
838 queries = ["""
839 SELECT
840 data,
841 field_label,
842 list_label
843 FROM ((
844 SELECT
845 pk_health_issue AS data,
846 description AS field_label,
847 description AS list_label
848 FROM clin.v_health_issues
849 WHERE
850 is_active IS true
851 AND
852 description %(fragment_condition)s
853 AND
854 %(ctxt_pat)s
855
856 ) UNION (
857
858 SELECT
859 pk_health_issue AS data,
860 description AS field_label,
861 description || _(' (inactive)') AS list_label
862 FROM clin.v_health_issues
863 WHERE
864 is_active IS false
865 AND
866 description %(fragment_condition)s
867 AND
868 %(ctxt_pat)s
869 )) AS union_query
870 ORDER BY
871 list_label"""
872 ],
873 context = ctxt
874 )
875 try: kwargs['patient_id']
876 except KeyError: kwargs['patient_id'] = None
877
878 if kwargs['patient_id'] is None:
879 self.use_current_patient = True
880 self.__register_patient_change_signals()
881 pat = gmPerson.gmCurrentPatient()
882 if pat.connected:
883 mp.set_context('pat', pat.ID)
884 else:
885 self.use_current_patient = False
886 self.__patient_id = int(kwargs['patient_id'])
887 mp.set_context('pat', self.__patient_id)
888
889 del kwargs['patient_id']
890
891 gmPhraseWheel.cPhraseWheel.__init__ (
892 self,
893 *args,
894 **kwargs
895 )
896 self.matcher = mp
897 #--------------------------------------------------------
898 # external API
899 #--------------------------------------------------------
901 if self.use_current_patient:
902 return False
903 self.__patient_id = int(patient_id)
904 self.set_context('pat', self.__patient_id)
905 return True
906 #--------------------------------------------------------
908 issue_name = self.GetValue().strip()
909 if issue_name == '':
910 gmDispatcher.send(signal = 'statustext', msg = _('Cannot create health issue without name.'), beep = True)
911 _log.debug('cannot create health issue without name')
912 return
913
914 if self.use_current_patient:
915 pat = gmPerson.gmCurrentPatient()
916 else:
917 pat = gmPerson.cPatient(aPK_obj = self.__patient_id)
918
919 emr = pat.emr
920 issue = emr.add_health_issue(issue_name = issue_name)
921
922 if issue is None:
923 self.data = {}
924 else:
925 self.SetText (
926 value = issue_name,
927 data = issue['pk_health_issue']
928 )
929 #--------------------------------------------------------
932 #--------------------------------------------------------
934 if self.GetData() is None:
935 return None
936 issue = self._data2instance()
937 if issue is None:
938 return None
939 return issue.format (
940 patient = None,
941 with_summary = True,
942 with_codes = False,
943 with_episodes = True,
944 with_encounters = True,
945 with_medications = False,
946 with_hospital_stays = False,
947 with_procedures = False,
948 with_family_history = False,
949 with_documents = False,
950 with_tests = False,
951 with_vaccinations = False,
952 with_external_care = True
953 )
954 #--------------------------------------------------------
955 # internal API
956 #--------------------------------------------------------
958 gmDispatcher.connect(self._pre_patient_unselection, 'pre_patient_unselection')
959 gmDispatcher.connect(self._post_patient_selection, 'post_patient_selection')
960 #--------------------------------------------------------
963 #--------------------------------------------------------
965 if self.use_current_patient:
966 patient = gmPerson.gmCurrentPatient()
967 self.set_context('pat', patient.ID)
968 return True
969 #------------------------------------------------------------
970 from Gnumed.wxGladeWidgets import wxgIssueSelectionDlg
971
973
975 try:
976 msg = kwargs['message']
977 except KeyError:
978 msg = None
979 del kwargs['message']
980 wxgIssueSelectionDlg.wxgIssueSelectionDlg.__init__(self, *args, **kwargs)
981 if msg is not None:
982 self._lbl_message.SetLabel(label=msg)
983 #--------------------------------------------------------
994 #------------------------------------------------------------
995 from Gnumed.wxGladeWidgets import wxgHealthIssueEditAreaPnl
996
997 -class cHealthIssueEditAreaPnl(gmEditArea.cGenericEditAreaMixin, wxgHealthIssueEditAreaPnl.wxgHealthIssueEditAreaPnl):
998 """Panel encapsulating health issue edit area functionality."""
999
1001
1002 try:
1003 data = kwargs['issue']
1004 del kwargs['issue']
1005 except KeyError:
1006 data = None
1007
1008 wxgHealthIssueEditAreaPnl.wxgHealthIssueEditAreaPnl.__init__(self, *args, **kwargs)
1009 gmEditArea.cGenericEditAreaMixin.__init__(self)
1010
1011 self.mode = 'new'
1012 self.data = data
1013 if data is not None:
1014 self.mode = 'edit'
1015
1016 self.__init_ui()
1017 #----------------------------------------------------------------
1019
1020 # FIXME: include more sources: coding systems/other database columns
1021 mp = gmMatchProvider.cMatchProvider_SQL2 (
1022 queries = ["SELECT DISTINCT ON (description) description, description FROM clin.health_issue WHERE description %(fragment_condition)s LIMIT 50"]
1023 )
1024 mp.setThresholds(1, 3, 5)
1025 self._PRW_condition.matcher = mp
1026
1027 mp = gmMatchProvider.cMatchProvider_SQL2 (
1028 queries = ["""
1029 SELECT DISTINCT ON (grouping) grouping, grouping from (
1030
1031 SELECT rank, grouping from ((
1032
1033 SELECT
1034 grouping,
1035 1 as rank
1036 from
1037 clin.health_issue
1038 where
1039 grouping %%(fragment_condition)s
1040 and
1041 (SELECT True from clin.encounter where fk_patient = %s and pk = clin.health_issue.fk_encounter)
1042
1043 ) union (
1044
1045 SELECT
1046 grouping,
1047 2 as rank
1048 from
1049 clin.health_issue
1050 where
1051 grouping %%(fragment_condition)s
1052
1053 )) as union_result
1054
1055 order by rank
1056
1057 ) as order_result
1058
1059 limit 50""" % gmPerson.gmCurrentPatient().ID
1060 ]
1061 )
1062 mp.setThresholds(1, 3, 5)
1063 self._PRW_grouping.matcher = mp
1064
1065 self._PRW_age_noted.add_callback_on_lose_focus(self._on_leave_age_noted)
1066 self._PRW_year_noted.add_callback_on_lose_focus(self._on_leave_year_noted)
1067
1068 # self._PRW_age_noted.add_callback_on_modified(self._on_modified_age_noted)
1069 # self._PRW_year_noted.add_callback_on_modified(self._on_modified_year_noted)
1070
1071 self._PRW_year_noted.Enable(True)
1072
1073 self._PRW_codes.add_callback_on_lose_focus(self._on_leave_codes)
1074
1075 #----------------------------------------------------------------
1076 # generic Edit Area mixin API
1077 #----------------------------------------------------------------
1079
1080 if self._PRW_condition.GetValue().strip() == '':
1081 self._PRW_condition.display_as_valid(False)
1082 self._PRW_condition.SetFocus()
1083 return False
1084 self._PRW_condition.display_as_valid(True)
1085 self._PRW_condition.Refresh()
1086
1087 # FIXME: sanity check age/year diagnosed
1088 age_noted = self._PRW_age_noted.GetValue().strip()
1089 if age_noted != '':
1090 if gmDateTime.str2interval(str_interval = age_noted) is None:
1091 self._PRW_age_noted.display_as_valid(False)
1092 self._PRW_age_noted.SetFocus()
1093 return False
1094 self._PRW_age_noted.display_as_valid(True)
1095 return True
1096 #----------------------------------------------------------------
1098 pat = gmPerson.gmCurrentPatient()
1099 emr = pat.emr
1100
1101 issue = emr.add_health_issue(issue_name = self._PRW_condition.GetValue().strip())
1102
1103 side = ''
1104 if self._ChBOX_left.GetValue():
1105 side += 's'
1106 if self._ChBOX_right.GetValue():
1107 side += 'd'
1108 issue['laterality'] = side
1109
1110 issue['summary'] = self._TCTRL_status.GetValue().strip()
1111 issue['diagnostic_certainty_classification'] = self._PRW_certainty.GetData()
1112 issue['grouping'] = self._PRW_grouping.GetValue().strip()
1113 issue['is_active'] = self._ChBOX_active.GetValue()
1114 issue['clinically_relevant'] = self._ChBOX_relevant.GetValue()
1115 issue['is_confidential'] = self._ChBOX_confidential.GetValue()
1116 issue['is_cause_of_death'] = self._ChBOX_caused_death.GetValue()
1117
1118 age_noted = self._PRW_age_noted.GetData()
1119 if age_noted is not None:
1120 issue['age_noted'] = age_noted
1121
1122 issue.save()
1123
1124 issue.generic_codes = [ c['data'] for c in self._PRW_codes.GetData() ]
1125
1126 self.data = issue
1127 return True
1128 #----------------------------------------------------------------
1130
1131 self.data['description'] = self._PRW_condition.GetValue().strip()
1132
1133 side = ''
1134 if self._ChBOX_left.GetValue():
1135 side += 's'
1136 if self._ChBOX_right.GetValue():
1137 side += 'd'
1138 self.data['laterality'] = side
1139
1140 self.data['summary'] = self._TCTRL_status.GetValue().strip()
1141 self.data['diagnostic_certainty_classification'] = self._PRW_certainty.GetData()
1142 self.data['grouping'] = self._PRW_grouping.GetValue().strip()
1143 self.data['is_active'] = bool(self._ChBOX_active.GetValue())
1144 self.data['clinically_relevant'] = bool(self._ChBOX_relevant.GetValue())
1145 self.data['is_confidential'] = bool(self._ChBOX_confidential.GetValue())
1146 self.data['is_cause_of_death'] = bool(self._ChBOX_caused_death.GetValue())
1147
1148 age_noted = self._PRW_age_noted.GetData()
1149 if age_noted is not None:
1150 self.data['age_noted'] = age_noted
1151
1152 self.data.save()
1153 self.data.generic_codes = [ c['data'] for c in self._PRW_codes.GetData() ]
1154
1155 return True
1156 #----------------------------------------------------------------
1158 self._PRW_condition.SetText()
1159 self._ChBOX_left.SetValue(0)
1160 self._ChBOX_right.SetValue(0)
1161 self._PRW_codes.SetText()
1162 self._on_leave_codes()
1163 self._PRW_certainty.SetText()
1164 self._PRW_grouping.SetText()
1165 self._TCTRL_status.SetValue('')
1166 self._PRW_age_noted.SetText()
1167 self._PRW_year_noted.SetText()
1168 self._ChBOX_active.SetValue(1)
1169 self._ChBOX_relevant.SetValue(1)
1170 self._ChBOX_confidential.SetValue(0)
1171 self._ChBOX_caused_death.SetValue(0)
1172
1173 self._PRW_condition.SetFocus()
1174 return True
1175 #----------------------------------------------------------------
1177 self._PRW_condition.SetText(self.data['description'])
1178
1179 lat = gmTools.coalesce(self.data['laterality'], '')
1180 if lat.find('s') == -1:
1181 self._ChBOX_left.SetValue(0)
1182 else:
1183 self._ChBOX_left.SetValue(1)
1184 if lat.find('d') == -1:
1185 self._ChBOX_right.SetValue(0)
1186 else:
1187 self._ChBOX_right.SetValue(1)
1188
1189 val, data = self._PRW_codes.generic_linked_codes2item_dict(self.data.generic_codes)
1190 self._PRW_codes.SetText(val, data)
1191 self._on_leave_codes()
1192
1193 if self.data['diagnostic_certainty_classification'] is not None:
1194 self._PRW_certainty.SetData(data = self.data['diagnostic_certainty_classification'])
1195 self._PRW_grouping.SetText(gmTools.coalesce(self.data['grouping'], ''))
1196 self._TCTRL_status.SetValue(gmTools.coalesce(self.data['summary'], ''))
1197
1198 if self.data['age_noted'] is None:
1199 self._PRW_age_noted.SetText()
1200 else:
1201 self._PRW_age_noted.SetText (
1202 value = '%sd' % self.data['age_noted'].days,
1203 data = self.data['age_noted']
1204 )
1205
1206 self._ChBOX_active.SetValue(self.data['is_active'])
1207 self._ChBOX_relevant.SetValue(self.data['clinically_relevant'])
1208 self._ChBOX_confidential.SetValue(self.data['is_confidential'])
1209 self._ChBOX_caused_death.SetValue(self.data['is_cause_of_death'])
1210
1211 self._TCTRL_status.SetFocus()
1212
1213 return True
1214 #----------------------------------------------------------------
1217 #--------------------------------------------------------
1218 # internal helpers
1219 #--------------------------------------------------------
1221 if not self._PRW_codes.IsModified():
1222 return True
1223
1224 self._TCTRL_code_details.SetValue('- ' + '\n- '.join([ c['list_label'] for c in self._PRW_codes.GetData() ]))
1225 #--------------------------------------------------------
1227
1228 if not self._PRW_age_noted.IsModified():
1229 return True
1230
1231 age_str = self._PRW_age_noted.GetValue().strip()
1232
1233 if age_str == '':
1234 return True
1235
1236 issue_age = gmDateTime.str2interval(str_interval = age_str)
1237
1238 if issue_age is None:
1239 self.status_message = _('Cannot parse [%s] into valid interval.') % age_str
1240 self._PRW_age_noted.display_as_valid(False)
1241 return True
1242
1243 pat = gmPerson.gmCurrentPatient()
1244 if pat['dob'] is not None:
1245 max_issue_age = pydt.datetime.now(tz=pat['dob'].tzinfo) - pat['dob']
1246 if issue_age >= max_issue_age:
1247 self.status_message = _('Health issue cannot have been noted at age %s. Patient is only %s old.') % (issue_age, pat.get_medical_age())
1248 self._PRW_age_noted.display_as_valid(False)
1249 return True
1250
1251 self._PRW_age_noted.display_as_valid(True)
1252 self._PRW_age_noted.SetText(value = age_str, data = issue_age)
1253
1254 if pat['dob'] is not None:
1255 fts = gmDateTime.cFuzzyTimestamp (
1256 timestamp = pat['dob'] + issue_age,
1257 accuracy = gmDateTime.acc_months
1258 )
1259 self._PRW_year_noted.SetText(value = str(fts), data = fts)
1260
1261 return True
1262 #--------------------------------------------------------
1264
1265 if not self._PRW_year_noted.IsModified():
1266 return True
1267
1268 year_noted = self._PRW_year_noted.GetData()
1269
1270 if year_noted is None:
1271 if self._PRW_year_noted.GetValue().strip() == '':
1272 self._PRW_year_noted.display_as_valid(True)
1273 return True
1274 self._PRW_year_noted.display_as_valid(False)
1275 return True
1276
1277 year_noted = year_noted.get_pydt()
1278
1279 if year_noted >= pydt.datetime.now(tz = year_noted.tzinfo):
1280 self.status_message = _('Condition diagnosed in the future.')
1281 self._PRW_year_noted.display_as_valid(False)
1282 return True
1283
1284 self._PRW_year_noted.display_as_valid(True)
1285
1286 pat = gmPerson.gmCurrentPatient()
1287 if pat['dob'] is not None:
1288 issue_age = year_noted - pat['dob']
1289 age_str = gmDateTime.format_interval_medically(interval = issue_age)
1290 self._PRW_age_noted.SetText(age_str, issue_age, True)
1291
1292 return True
1293 #--------------------------------------------------------
1297 #--------------------------------------------------------
1301 #================================================================
1302 # diagnostic certainty related widgets/functions
1303 #----------------------------------------------------------------
1305
1307
1308 gmPhraseWheel.cPhraseWheel.__init__(self, *args, **kwargs)
1309
1310 self.selection_only = False # can be NULL, too
1311
1312 mp = gmMatchProvider.cMatchProvider_FixedList (
1313 aSeq = [
1314 {'data': 'A', 'list_label': gmEMRStructItems.diagnostic_certainty_classification2str('A'), 'field_label': gmEMRStructItems.diagnostic_certainty_classification2str('A'), 'weight': 1},
1315 {'data': 'B', 'list_label': gmEMRStructItems.diagnostic_certainty_classification2str('B'), 'field_label': gmEMRStructItems.diagnostic_certainty_classification2str('B'), 'weight': 1},
1316 {'data': 'C', 'list_label': gmEMRStructItems.diagnostic_certainty_classification2str('C'), 'field_label': gmEMRStructItems.diagnostic_certainty_classification2str('C'), 'weight': 1},
1317 {'data': 'D', 'list_label': gmEMRStructItems.diagnostic_certainty_classification2str('D'), 'field_label': gmEMRStructItems.diagnostic_certainty_classification2str('D'), 'weight': 1}
1318 ]
1319 )
1320 mp.setThresholds(1, 2, 4)
1321 self.matcher = mp
1322
1323 self.SetToolTip(_(
1324 "The diagnostic classification or grading of this assessment.\n"
1325 "\n"
1326 "This documents how certain one is about this being a true diagnosis."
1327 ))
1328
1329 #================================================================
1330 # MAIN
1331 #----------------------------------------------------------------
1332 if __name__ == '__main__':
1333
1334 if len(sys.argv) < 2:
1335 sys.exit()
1336
1337 if sys.argv[1] != 'test':
1338 sys.exit()
1339
1340 from Gnumed.business import gmPersonSearch
1341 from Gnumed.wxpython import gmPatSearchWidgets
1342
1343 #================================================================
1345 """
1346 Test application for testing EMR struct widgets
1347 """
1348 #--------------------------------------------------------
1350 """
1351 Create test application UI
1352 """
1353 frame = wx.Frame (
1354 None,
1355 -4,
1356 'Testing EMR struct widgets',
1357 size=wx.Size(600, 400),
1358 style=wx.DEFAULT_FRAME_STYLE | wx.NO_FULL_REPAINT_ON_RESIZE
1359 )
1360 filemenu = wx.Menu()
1361 filemenu.AppendSeparator()
1362 item = filemenu.Append(ID_EXIT, "E&xit"," Terminate test application")
1363 self.Bind(wx.EVT_MENU, self.OnCloseWindow, item)
1364
1365 # Creating the menubar.
1366 menuBar = wx.MenuBar()
1367 menuBar.Append(filemenu,"&File")
1368
1369 frame.SetMenuBar(menuBar)
1370
1371 txt = wx.StaticText( frame, -1, _("Select desired test option from the 'File' menu"),
1372 wx.DefaultPosition, wx.DefaultSize, 0 )
1373
1374 # patient EMR
1375 self.__pat = gmPerson.gmCurrentPatient()
1376
1377 frame.Show(1)
1378 return 1
1379 #--------------------------------------------------------
1385
1386 #----------------------------------------------------------------
1388 app = wx.PyWidgetTester(size = (200, 300))
1389 emr = pat.emr
1390 epi = emr.get_episodes()[0]
1391 pnl = cEpisodeEditAreaPnl(app.frame, -1, episode=epi)
1392 app.frame.Show(True)
1393 app.MainLoop()
1394 #----------------------------------------------------------------
1396 app = wx.PyWidgetTester(size = (200, 300))
1397 emr = pat.emr
1398 epi = emr.get_episodes()[0]
1399 edit_episode(parent=app.frame, episode=epi)
1400
1401 #----------------------------------------------------------------
1403 app = wx.PyWidgetTester(size = (400, 40))
1404 app.SetWidget(cEpisodeSelectionPhraseWheel, id=-1, size=(180,20), pos=(10,20))
1405 # app.SetWidget(cEpisodeSelectionPhraseWheel, id=-1, size=(350,20), pos=(10,20), patient_id=pat.ID)
1406 app.MainLoop()
1407
1408 #----------------------------------------------------------------
1410 app = wx.PyWidgetTester(size = (200, 300))
1411 edit_health_issue(parent=app.frame, issue=None)
1412
1413 #----------------------------------------------------------------
1415 app = wx.PyWidgetTester(size = (200, 300))
1416 app.SetWidget(cHealthIssueEditAreaPnl, id=-1, size = (400,400))
1417 app.MainLoop()
1418
1419 #================================================================
1420
1421 # obtain patient
1422 pat = gmPersonSearch.ask_for_patient()
1423 if pat is None:
1424 print("No patient. Exiting gracefully...")
1425 sys.exit(0)
1426 gmPatSearchWidgets.set_active_patient(patient=pat)
1427
1428 # try:
1429 # lauch emr dialogs test application
1430 # app = testapp(0)
1431 # app.MainLoop()
1432 # except Exception:
1433 # _log.exception("unhandled exception caught !")
1434 # but re-raise them
1435 # raise
1436
1437 #test_epsiode_edit_area_pnl()
1438 #test_episode_edit_area_dialog()
1439 #test_health_issue_edit_area_dlg()
1440 test_episode_selection_prw()
1441
| Home | Trees | Indices | Help |
|
|---|
| Generated by Epydoc 3.0.1 on Fri Jan 25 02:55:27 2019 | http://epydoc.sourceforge.net |