| Home | Trees | Indices | Help |
|
|---|
|
|
1 """GNUmed immunisation/vaccination widgets.
2
3 Modelled after Richard Terry's design document.
4
5 copyright: authors
6 """
7 #======================================================================
8 __author__ = "R.Terry, S.J.Tan, K.Hilbert"
9 __license__ = "GPL v2 or later (details at http://www.gnu.org)"
10
11 import sys
12 import logging
13
14
15 import wx
16
17
18 if __name__ == '__main__':
19 sys.path.insert(0, '../../')
20 from Gnumed.pycommon import gmDispatcher
21 from Gnumed.pycommon import gmMatchProvider
22 from Gnumed.pycommon import gmTools
23 from Gnumed.pycommon import gmI18N
24 from Gnumed.pycommon import gmCfg
25 from Gnumed.pycommon import gmCfg2
26 from Gnumed.pycommon import gmDateTime
27 from Gnumed.pycommon import gmNetworkTools
28 from Gnumed.pycommon import gmPrinting
29 from Gnumed.pycommon import gmPG2
30
31 from Gnumed.business import gmPerson
32 from Gnumed.business import gmVaccination
33 from Gnumed.business import gmPraxis
34 from Gnumed.business import gmProviderInbox
35
36 from Gnumed.wxpython import gmPhraseWheel
37 from Gnumed.wxpython import gmTerryGuiParts
38 from Gnumed.wxpython import gmRegetMixin
39 from Gnumed.wxpython import gmGuiHelpers
40 from Gnumed.wxpython import gmEditArea
41 from Gnumed.wxpython import gmListWidgets
42 from Gnumed.wxpython import gmFormWidgets
43 from Gnumed.wxpython import gmMacro
44 from Gnumed.wxpython import gmAuthWidgets
45 from Gnumed.wxpython import gmSubstanceMgmtWidgets
46
47
48 _log = logging.getLogger('gm.vacc')
49
50 #======================================================================
51 # vaccine related widgets
52 #----------------------------------------------------------------------
54
55 dbo_conn = gmAuthWidgets.get_dbowner_connection(procedure = _('Regenerating generic vaccines'))
56 if dbo_conn is None:
57 return False
58
59 wx.BeginBusyCursor()
60 _cfg = gmCfg2.gmCfgData()
61 sql_script = gmVaccination.write_generic_vaccine_sql (
62 'client-%s' % _cfg.get(option = 'client_version'),
63 include_indications_mapping = False
64 )
65 _log.debug('regenerating generic vaccines, SQL script: %s', sql_script)
66 if not gmPG2.run_sql_script(sql_script, conn = dbo_conn):
67 wx.EndBusyCursor()
68 gmGuiHelpers.gm_show_warning (
69 aMessage = _('Error regenerating generic vaccines.\n\nSee [%s]') % sql_script,
70 aTitle = _('Regenerating generic vaccines')
71 )
72 return False
73
74 gmDispatcher.send(signal = 'statustext', msg = _('Successfully regenerated generic vaccines ...'), beep = False)
75 wx.EndBusyCursor()
76 return True
77
78 #----------------------------------------------------------------------
80 ea = cVaccineEAPnl(parent, -1)
81 ea.data = vaccine
82 ea.mode = gmTools.coalesce(vaccine, 'new', 'edit')
83 dlg = gmEditArea.cGenericEditAreaDlg2(parent, -1, edit_area = ea, single_entry = single_entry)
84 dlg.SetTitle(gmTools.coalesce(vaccine, _('Adding new vaccine'), _('Editing vaccine')))
85 if dlg.ShowModal() == wx.ID_OK:
86 dlg.DestroyLater()
87 return True
88 dlg.DestroyLater()
89 return False
90
91 #----------------------------------------------------------------------
93
94 if parent is None:
95 parent = wx.GetApp().GetTopWindow()
96
97 # #------------------------------------------------------------
98 # def delete(vaccine=None):
99 # product = vaccine.product
100 # deleted = gmVaccination.delete_vaccine(vaccine = vaccine['pk_vaccine'])
101 # if not deleted:
102 # gmGuiHelpers.gm_show_info (
103 # _( 'Cannot delete vaccine\n'
104 # '\n'
105 # ' %s - %s (#%s)\n'
106 # '\n'
107 # 'It is probably documented in a vaccination.'
108 # ) % (
109 # vaccine['vaccine'],
110 # vaccine['l10n_preparation'],
111 # vaccine['pk_vaccine']
112 # ),
113 # _('Deleting vaccine')
114 # )
115 # return False
116 # delete_product = gmGuiHelpers.gm_show_question (
117 # title = _('Deleting vaccine'),
118 # question = _(
119 # u'Fully delete the vaccine (including the associated drug product) ?\n'
120 # u'\n'
121 # u' "%s" (%s)'
122 # ) % (product['product'], product['l10n_preparation'])
123 # )
124 # if delete_product:
125 # pass
126 # return True
127
128 #------------------------------------------------------------
129 def manage_drug_products(vaccine):
130 gmSubstanceMgmtWidgets.manage_drug_products(parent = parent)
131 return True
132
133 #------------------------------------------------------------
134 def edit(vaccine=None):
135 return edit_vaccine(parent = parent, vaccine = vaccine, single_entry = True)
136
137 #------------------------------------------------------------
138 def get_tooltip(vaccine):
139 if vaccine is None:
140 return None
141 return '\n'.join(vaccine.format())
142
143 #------------------------------------------------------------
144 def refresh(lctrl):
145 vaccines = gmVaccination.get_vaccines(order_by = 'vaccine')
146
147 items = [ [
148 '%s' % v['pk_drug_product'],
149 '%s%s' % (
150 v['vaccine'],
151 gmTools.bool2subst (
152 v['is_fake_vaccine'],
153 ' (%s)' % _('fake'),
154 ''
155 )
156 ),
157 v['l10n_preparation'],
158 gmTools.coalesce(v['atc_code'], ''),
159 '%s - %s' % (
160 gmTools.coalesce(v['min_age'], '?'),
161 gmTools.coalesce(v['max_age'], '?'),
162 ),
163 gmTools.coalesce(v['comment'], '')
164 ] for v in vaccines ]
165 lctrl.set_string_items(items)
166 lctrl.set_data(vaccines)
167
168 #------------------------------------------------------------
169 gmListWidgets.get_choices_from_list (
170 parent = parent,
171 caption = _('Showing vaccine details'),
172 columns = [ '#', _('Vaccine'), _('Preparation'), _('ATC'), _('Age range'), _('Comment') ],
173 single_selection = True,
174 refresh_callback = refresh,
175 edit_callback = edit,
176 new_callback = edit,
177 #delete_callback = delete,
178 list_tooltip_callback = get_tooltip,
179 left_extra_button = (_('Products'), _('Manage drug products'), manage_drug_products)
180 )
181
182 #----------------------------------------------------------------------
184
186
187 gmPhraseWheel.cPhraseWheel.__init__(self, *args, **kwargs)
188
189 context = {
190 'ctxt_vaccine': {
191 'where_part': 'AND pk_vaccine = %(pk_vaccine)s',
192 'placeholder': 'pk_vaccine'
193 }
194 }
195
196 query = """
197 SELECT data, field_label, list_label FROM (
198
199 SELECT distinct on (field_label)
200 data,
201 field_label,
202 list_label,
203 rank
204 FROM ((
205 -- batch_no by vaccine
206 SELECT
207 batch_no AS data,
208 batch_no AS field_label,
209 batch_no || ' (' || vaccine || ')' AS list_label,
210 1 as rank
211 FROM
212 clin.v_vaccinations
213 WHERE
214 batch_no %(fragment_condition)s
215 %(ctxt_vaccine)s
216 ) UNION ALL (
217 -- batch_no for any vaccine
218 SELECT
219 batch_no AS data,
220 batch_no AS field_label,
221 batch_no || ' (' || vaccine || ')' AS list_label,
222 2 AS rank
223 FROM
224 clin.v_vaccinations
225 WHERE
226 batch_no %(fragment_condition)s
227 )
228
229 ) AS matching_batch_nos
230
231 ) as unique_matches
232
233 ORDER BY rank, list_label
234 LIMIT 25
235 """
236 mp = gmMatchProvider.cMatchProvider_SQL2(queries = query, context = context)
237 mp.setThresholds(1, 2, 3)
238 self.matcher = mp
239
240 self.unset_context(context = 'pk_vaccine')
241 self.SetToolTip(_('Enter or select the batch/lot number of the vaccine used.'))
242 self.selection_only = False
243
244 #----------------------------------------------------------------------
246
248
249 gmPhraseWheel.cPhraseWheel.__init__(self, *args, **kwargs)
250
251 # consider ATCs in ref.drug_product and ref.vacc_indication
252 query = """
253 SELECT data, list_label, field_label FROM (
254
255 SELECT DISTINCT ON (data)
256 data,
257 list_label,
258 field_label
259 FROM ((
260 -- fragment -> vaccine
261 SELECT
262 r_v_v.pk_vaccine
263 AS data,
264 r_v_v.vaccine || ' ('
265 || (SELECT string_agg(l10n_inds.ind_desc::text, ', ') FROM (
266 SELECT unnest(r_v_v.indications)->>'l10n_indication' AS ind_desc
267 ) AS l10n_inds)
268 || ')'
269 AS list_label,
270 r_v_v.vaccine
271 AS field_label
272 FROM
273 ref.v_vaccines r_v_v
274 WHERE
275 r_v_v.vaccine %(fragment_condition)s
276
277 ) union all (
278
279 -- fragment -> localized indication -> vaccines
280 SELECT
281 r_vi4v.pk_vaccine
282 AS data,
283 r_vi4v.vaccine || ' ('
284 || (SELECT string_agg(l10n_inds.ind_desc::text, ', ') FROM (
285 SELECT unnest(r_vi4v.indications)->>'l10n_indication' AS ind_desc
286 ) AS l10n_inds)
287 || ')'
288 AS list_label,
289 r_vi4v.vaccine
290 AS field_label
291 FROM
292 ref.v_indications4vaccine r_vi4v
293 WHERE
294 r_vi4v.l10n_indication %(fragment_condition)s
295
296 ) union all (
297
298 -- fragment -> indication -> vaccines
299 SELECT
300 r_vi4v.pk_vaccine
301 AS data,
302 r_vi4v.vaccine || ' ('
303 || (SELECT string_agg(l10n_inds.ind_desc::text, ', ') FROM (
304 SELECT unnest(r_vi4v.indications)->>'l10n_indication' AS ind_desc
305 ) AS l10n_inds)
306 || ')'
307 AS list_label,
308 r_vi4v.vaccine
309 AS field_label
310 FROM
311 ref.v_indications4vaccine r_vi4v
312 WHERE
313 r_vi4v.indication %(fragment_condition)s
314 )
315 ) AS distinct_total
316
317 ) AS total
318
319 ORDER by list_label
320 LIMIT 25
321 """
322 mp = gmMatchProvider.cMatchProvider_SQL2(queries = query)
323 mp.setThresholds(1, 2, 3)
324 self.matcher = mp
325
326 self.selection_only = True
327 #------------------------------------------------------------------
330
331 #----------------------------------------------------------------------
332 from Gnumed.wxGladeWidgets import wxgVaccineEAPnl
333
335
337 try:
338 data = kwargs['vaccine']
339 del kwargs['vaccine']
340 except KeyError:
341 data = None
342
343 wxgVaccineEAPnl.wxgVaccineEAPnl.__init__(self, *args, **kwargs)
344 gmEditArea.cGenericEditAreaMixin.__init__(self)
345
346 self.mode = 'new'
347 self.data = data
348 if data is not None:
349 self.mode = 'edit'
350
351 self.__indications = None
352
353 #----------------------------------------------------------------
355 self._TCTRL_indications.SetValue('')
356 if self.data is None:
357 return
358 self._TCTRL_indications.SetValue('- ' + '\n- '.join([ i['l10n_indication'] for i in self.data['indications'] ]))
359
360 #----------------------------------------------------------------
361 # generic Edit Area mixin API
362 #----------------------------------------------------------------
364
365 has_errors = False
366
367 if self._PRW_drug_product.GetValue().strip() == '':
368 has_errors = True
369 self._PRW_drug_product.display_as_valid(False)
370 else:
371 self._PRW_drug_product.display_as_valid(True)
372
373 atc = self._PRW_atc.GetValue().strip()
374 if (atc == '') or (atc.startswith('J07')):
375 self._PRW_atc.display_as_valid(True)
376 else:
377 if self._PRW_atc.GetData() is None:
378 self._PRW_atc.display_as_valid(True)
379 else:
380 has_errors = True
381 self._PRW_atc.display_as_valid(False)
382
383 val = self._PRW_age_min.GetValue().strip()
384 if val == '':
385 self._PRW_age_min.display_as_valid(True)
386 else:
387 if gmDateTime.str2interval(val) is None:
388 has_errors = True
389 self._PRW_age_min.display_as_valid(False)
390 else:
391 self._PRW_age_min.display_as_valid(True)
392
393 val = self._PRW_age_max.GetValue().strip()
394 if val == '':
395 self._PRW_age_max.display_as_valid(True)
396 else:
397 if gmDateTime.str2interval(val) is None:
398 has_errors = True
399 self._PRW_age_max.display_as_valid(False)
400 else:
401 self._PRW_age_max.display_as_valid(True)
402
403 # complex conditions
404 # are we editing ?
405 if self.mode == 'edit':
406 change_of_product = self.data['pk_drug_product'] != self._PRW_drug_product.GetData()
407 if change_of_product and self.data.is_in_use:
408 do_it = gmGuiHelpers.gm_show_question (
409 aTitle = _('Saving vaccine'),
410 aMessage = _(
411 'This vaccine is already in use:\n'
412 '\n'
413 ' "%s"\n'
414 '\n'
415 'Are you absolutely positively sure that\n'
416 'you really want to edit this vaccine ?\n'
417 '\n'
418 'This will change the vaccine name and/or target\n'
419 'conditions in each patient this vaccine was\n'
420 'used in to document a vaccination with.\n'
421 ) % self._PRW_drug_product.GetValue().strip()
422 )
423 if not do_it:
424 has_errors = True
425 else:
426 if self._PRW_drug_product.GetData() is None:
427 # need to ask for indications ?
428 if self._PRW_drug_product.GetValue().strip() != '':
429 self.__indications = gmSubstanceMgmtWidgets.manage_substance_doses(vaccine_indications_only = True)
430 if self.__indications is None:
431 has_errors = True
432 else:
433 # existing drug product selected
434 pass
435
436 return (has_errors is False)
437
438 #----------------------------------------------------------------
440
441 # save the data as a new instance
442 vaccine = gmVaccination.create_vaccine (
443 pk_drug_product = self._PRW_drug_product.GetData(),
444 product_name = self._PRW_drug_product.GetValue().strip(),
445 indications = self.__indications,
446 is_live = self._CHBOX_live.GetValue()
447 )
448 val = self._PRW_age_min.GetValue().strip()
449 if val != '':
450 vaccine['min_age'] = gmDateTime.str2interval(val)
451 val = self._PRW_age_max.GetValue().strip()
452 if val != '':
453 vaccine['max_age'] = gmDateTime.str2interval(val)
454 val = self._TCTRL_comment.GetValue().strip()
455 if val != '':
456 vaccine['comment'] = val
457 vaccine.save()
458
459 drug = vaccine.product
460 drug['is_fake_product'] = self._CHBOX_fake.GetValue()
461 val = self._PRW_atc.GetData()
462 if val is not None:
463 if val != 'J07':
464 drug['atc'] = val.strip()
465 drug.save()
466
467 # must be done very late or else the property access
468 # will refresh the display such that later field
469 # access will return empty values
470 self.data = vaccine
471
472 return True
473
474 #----------------------------------------------------------------
476
477 drug = self.data.product
478 drug['product'] = self._PRW_drug_product.GetValue().strip()
479 drug['is_fake_product'] = self._CHBOX_fake.GetValue()
480 val = self._PRW_atc.GetData()
481 if val is not None:
482 if val != 'J07':
483 drug['atc'] = val.strip()
484 drug.save()
485
486 self.data['is_live'] = self._CHBOX_live.GetValue()
487 val = self._PRW_age_min.GetValue().strip()
488 if val != '':
489 self.data['min_age'] = gmDateTime.str2interval(val)
490 if val != '':
491 self.data['max_age'] = gmDateTime.str2interval(val)
492 val = self._TCTRL_comment.GetValue().strip()
493 if val != '':
494 self.data['comment'] = val
495 self.data.save()
496
497 return True
498
499 #----------------------------------------------------------------
501 self._PRW_drug_product.SetText(value = '', data = None, suppress_smarts = True)
502 self._CHBOX_live.SetValue(False)
503 self._CHBOX_fake.SetValue(False)
504 self._PRW_atc.SetText(value = '', data = None, suppress_smarts = True)
505 self._PRW_age_min.SetText(value = '', data = None, suppress_smarts = True)
506 self._PRW_age_max.SetText(value = '', data = None, suppress_smarts = True)
507 self._TCTRL_comment.SetValue('')
508
509 self.__refresh_indications()
510
511 self._PRW_drug_product.SetFocus()
512
513 #----------------------------------------------------------------
515 self._PRW_drug_product.SetText(value = self.data['vaccine'], data = self.data['pk_drug_product'])
516 self._CHBOX_live.SetValue(self.data['is_live'])
517 self._CHBOX_fake.SetValue(self.data['is_fake_vaccine'])
518 self._PRW_atc.SetText(value = self.data['atc_code'], data = self.data['atc_code'])
519 if self.data['min_age'] is None:
520 self._PRW_age_min.SetText(value = '', data = None, suppress_smarts = True)
521 else:
522 self._PRW_age_min.SetText (
523 value = gmDateTime.format_interval(self.data['min_age'], gmDateTime.acc_years),
524 data = self.data['min_age']
525 )
526 if self.data['max_age'] is None:
527 self._PRW_age_max.SetText(value = '', data = None, suppress_smarts = True)
528 else:
529 self._PRW_age_max.SetText (
530 value = gmDateTime.format_interval(self.data['max_age'], gmDateTime.acc_years),
531 data = self.data['max_age']
532 )
533 self._TCTRL_comment.SetValue(gmTools.coalesce(self.data['comment'], ''))
534
535 self.__refresh_indications()
536
537 self._PRW_drug_product.SetFocus()
538
539 #----------------------------------------------------------------
542
543 #======================================================================
544 # vaccination related widgets
545 #----------------------------------------------------------------------
547
548 def is_valid(value):
549 value = value.strip()
550 if value == '':
551 return True, gmVaccination.URL_vaccine_adr_german_default
552 try:
553 urllib.request.urlopen(value)
554 return True, value
555 except Exception: # FIXME: more specific
556 return True, value
557
558 gmCfgWidgets.configure_string_option (
559 message = _(
560 'GNUmed will use this URL to access a website which lets\n'
561 'you report an adverse vaccination reaction (vADR).\n'
562 '\n'
563 'If you set it to a specific address that URL must be\n'
564 'accessible now. If you leave it empty it will fall back\n'
565 'to the URL for reporting other adverse drug reactions.'
566 ),
567 option = 'external.urls.report_vaccine_ADR',
568 bias = 'user',
569 default_value = gmVaccination.URL_vaccine_adr_german_default,
570 validator = is_valid
571 )
572
573 #----------------------------------------------------------------------
575
576 def is_valid(value):
577 value = value.strip()
578 if value == '':
579 return True, gmVaccination.URL_vaccination_plan
580 try:
581 urllib.request.urlopen(value)
582 return True, value
583 except Exception: # FIXME: more specific
584 return True, value
585
586 gmCfgWidgets.configure_string_option (
587 message = _(
588 'GNUmed will use this URL to access a page showing\n'
589 'vaccination schedules.\n'
590 '\n'
591 'You can leave this empty but to set it to a specific\n'
592 'address the URL must be accessible now.'
593 ),
594 option = 'external.urls.vaccination_plans',
595 bias = 'user',
596 default_value = gmVaccination.URL_vaccination_plan,
597 validator = is_valid
598 )
599
600 #----------------------------------------------------------------------
602
603 if parent is None:
604 parent = wx.GetApp().GetTopWindow()
605
606 vaccs_printout = gmFormWidgets.generate_form_from_template (
607 parent = parent,
608 template_types = [
609 'Medical statement',
610 'vaccination report',
611 'vaccination record',
612 'reminder'
613 ],
614 edit = False
615 )
616
617 if vaccs_printout is None:
618 return False
619
620 return gmFormWidgets.act_on_generated_forms (
621 parent = parent,
622 forms = [vaccs_printout],
623 jobtype = 'vaccinations',
624 episode_name = 'administrative',
625 review_copy_as_normal = True
626 )
627
628 #----------------------------------------------------------------------
630 ea = cVaccinationEAPnl(parent, -1)
631 ea.data = vaccination
632 ea.mode = gmTools.coalesce(vaccination, 'new', 'edit')
633 dlg = gmEditArea.cGenericEditAreaDlg2(parent, -1, edit_area = ea, single_entry = single_entry)
634 dlg.SetTitle(gmTools.coalesce(vaccination, _('Adding new vaccinations'), _('Editing vaccination')))
635 if dlg.ShowModal() == wx.ID_OK:
636 dlg.DestroyLater()
637 return True
638 dlg.DestroyLater()
639 if not single_entry:
640 return True
641 return False
642
643 #----------------------------------------------------------------------
645
646 pat = gmPerson.gmCurrentPatient()
647 emr = pat.emr
648
649 if parent is None:
650 parent = wx.GetApp().GetTopWindow()
651
652 #------------------------------------------------------------
653 def browse2schedules(vaccination=None):
654 dbcfg = gmCfg.cCfgSQL()
655 url = dbcfg.get2 (
656 option = 'external.urls.vaccination_plans',
657 workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace,
658 bias = 'user',
659 default = gmVaccination.URL_vaccination_plan
660 )
661
662 gmNetworkTools.open_url_in_browser(url = url)
663 return False
664
665 #------------------------------------------------------------
666 def print_vaccs(vaccination=None):
667 print_vaccinations(parent = parent)
668 return False
669
670 #------------------------------------------------------------
671 def add_recall(vaccination=None):
672 if vaccination is None:
673 subject = _('vaccination recall')
674 else:
675 subject = _('vaccination recall (%s)') % vaccination['vaccine']
676
677 recall = gmProviderInbox.create_inbox_message (
678 message_type = _('Vaccination'),
679 subject = subject,
680 patient = pat.ID,
681 staff = None
682 )
683
684 if vaccination is not None:
685 recall['data'] = _('Existing vaccination:\n\n%s') % '\n'.join(vaccination.format(
686 with_indications = True,
687 with_comment = True,
688 with_reaction = False,
689 date_format = '%Y %b %d'
690 ))
691 recall.save()
692
693 from Gnumed.wxpython import gmProviderInboxWidgets
694 gmProviderInboxWidgets.edit_inbox_message (
695 parent = parent,
696 message = recall,
697 single_entry = False
698 )
699
700 return False
701
702 #------------------------------------------------------------
703 def get_tooltip(vaccination):
704 if vaccination is None:
705 return None
706 return '\n'.join(vaccination.format (
707 with_indications = True,
708 with_comment = True,
709 with_reaction = True,
710 date_format = '%Y %b %d'
711 ))
712
713 #------------------------------------------------------------
714 def edit(vaccination=None):
715 return edit_vaccination(parent = parent, vaccination = vaccination, single_entry = (vaccination is not None))
716
717 #------------------------------------------------------------
718 def delete(vaccination=None):
719 gmVaccination.delete_vaccination(vaccination = vaccination['pk_vaccination'])
720 return True
721
722 #------------------------------------------------------------
723 def refresh(lctrl):
724
725 items = []
726 data = []
727 if latest_only:
728 latest_vaccs = emr.get_latest_vaccinations()
729 for indication in sorted(latest_vaccs.keys()):
730 no_of_shots4ind, latest_vacc4ind = latest_vaccs[indication]
731 items.append ([
732 indication,
733 _('%s (latest of %s: %s ago)') % (
734 gmDateTime.pydt_strftime(latest_vacc4ind['date_given'], format = '%Y %b'),
735 no_of_shots4ind,
736 gmDateTime.format_interval_medically(gmDateTime.pydt_now_here() - latest_vacc4ind['date_given'])
737 ),
738 latest_vacc4ind['vaccine'],
739 latest_vacc4ind['batch_no'],
740 gmTools.coalesce(latest_vacc4ind['site'], ''),
741 gmTools.coalesce(latest_vacc4ind['reaction'], ''),
742 gmTools.coalesce(latest_vacc4ind['comment'], '')
743 ])
744 data.append(latest_vacc4ind)
745 else:
746 shots = emr.get_vaccinations(order_by = 'date_given DESC, pk_vaccination')
747 if expand_indications:
748 shots_by_ind = {}
749 for shot in shots:
750 for ind in shot['indications']:
751 try:
752 shots_by_ind[ind['l10n_indication']].append(shot)
753 except KeyError:
754 shots_by_ind[ind['l10n_indication']] = [shot]
755 for ind in sorted(shots_by_ind.keys()):
756 idx = len(shots_by_ind[ind])
757 for shot in shots_by_ind[ind]:
758 items.append ([
759 '%s (#%s)' % (ind, idx),
760 _('%s (%s ago)') % (
761 gmDateTime.pydt_strftime(shot['date_given'], '%Y %b %d'),
762 gmDateTime.format_interval_medically(gmDateTime.pydt_now_here() - shot['date_given'])
763 ),
764 shot['vaccine'],
765 shot['batch_no'],
766 gmTools.coalesce(shot['site'], ''),
767 gmTools.coalesce(shot['reaction'], ''),
768 gmTools.coalesce(shot['comment'], '')
769 ])
770 idx -= 1
771 data.append(shot)
772 else:
773 items = [ [
774 gmDateTime.pydt_strftime(s['date_given'], '%Y %b %d'),
775 s['vaccine'],
776 ', '.join([ i['l10n_indication'] for i in s['indications'] ]),
777 s['batch_no'],
778 gmTools.coalesce(s['site'], ''),
779 gmTools.coalesce(s['reaction'], ''),
780 gmTools.coalesce(s['comment'], '')
781 ] for s in shots ]
782 data = shots
783
784 lctrl.set_string_items(items)
785 lctrl.set_data(data)
786
787 #------------------------------------------------------------
788 if latest_only:
789 msg = _('Most recent vaccination for each indication.\n')
790 cols = [ _('Indication'), _('Date'), _('Vaccine'), _('Batch'), _('Site'), _('Reaction'), _('Comment') ]
791 else:
792 if expand_indications:
793 msg = _('Complete vaccination history (per indication).\n')
794 cols = [ _('Indication'), _('Date'), _('Vaccine'), _('Batch'), _('Site'), _('Reaction'), _('Comment') ]
795 else:
796 msg = _('Complete vaccination history (by shot).\n')
797 cols = [ _('Date'), _('Vaccine'), _('Intended to protect from'), _('Batch'), _('Site'), _('Reaction'), _('Comment') ]
798
799 gmListWidgets.get_choices_from_list (
800 parent = parent,
801 msg = msg,
802 caption = _('Showing vaccinations.'),
803 columns = cols,
804 single_selection = True,
805 refresh_callback = refresh,
806 new_callback = edit,
807 edit_callback = edit,
808 delete_callback = delete,
809 list_tooltip_callback = get_tooltip,
810 left_extra_button = (_('Print'), _('Print vaccinations or recalls.'), print_vaccs),
811 middle_extra_button = (_('Recall'), _('Add a recall for a vaccination'), add_recall),
812 right_extra_button = (_('Vx schedules'), _('Open a browser showing vaccination schedules.'), browse2schedules)
813 )
814
815 #----------------------------------------------------------------------
816 from Gnumed.wxGladeWidgets import wxgVaccinationEAPnl
817
818 -class cVaccinationEAPnl(wxgVaccinationEAPnl.wxgVaccinationEAPnl, gmEditArea.cGenericEditAreaMixin):
819 """
820 - warn on apparent duplicates
821 - ask if "missing" (= previous, non-recorded) vaccinations
822 should be estimated and saved (add note "auto-generated")
823
824 Batch No (http://www.fao.org/docrep/003/v9952E12.htm)
825 """
827
828 try:
829 data = kwargs['vaccination']
830 del kwargs['vaccination']
831 except KeyError:
832 data = None
833
834 wxgVaccinationEAPnl.wxgVaccinationEAPnl.__init__(self, *args, **kwargs)
835 gmEditArea.cGenericEditAreaMixin.__init__(self)
836
837 self.mode = 'new'
838 self.data = data
839 if data is not None:
840 self.mode = 'edit'
841
842 self.__init_ui()
843
844 #----------------------------------------------------------------
846 # adjust phrasewheels etc
847 self._PRW_vaccine.add_callback_on_lose_focus(self._on_PRW_vaccine_lost_focus)
848 self._PRW_provider.selection_only = False
849 self._PRW_reaction.add_callback_on_lose_focus(self._on_PRW_reaction_lost_focus)
850
851 #----------------------------------------------------------------
853
854 vaccine = self._PRW_vaccine.GetData(as_instance=True)
855
856 if self.mode == 'edit':
857 if vaccine is None:
858 self._PRW_batch.unset_context(context = 'pk_vaccine')
859 else:
860 self._PRW_batch.set_context(context = 'pk_vaccine', val = vaccine['pk_vaccine'])
861 # we are entering a new vaccination
862 else:
863 if vaccine is None:
864 self._PRW_batch.unset_context(context = 'pk_vaccine')
865 else:
866 self._PRW_batch.set_context(context = 'pk_vaccine', val = vaccine['pk_vaccine'])
867
868 self.__refresh_indications()
869
870 #----------------------------------------------------------------
872 if self._PRW_reaction.GetValue().strip() == '':
873 self._BTN_report.Enable(False)
874 else:
875 self._BTN_report.Enable(True)
876
877 #----------------------------------------------------------------
879 self._TCTRL_indications.SetValue('')
880 vaccine = self._PRW_vaccine.GetData(as_instance = True)
881 if vaccine is None:
882 return
883 lines = []
884 emr = gmPerson.gmCurrentPatient().emr
885 latest_vaccs = emr.get_latest_vaccinations (
886 atc_indications = [ i['atc_indication'] for i in vaccine['indications'] ]
887 )
888 for l10n_ind in [ i['l10n_indication'] for i in vaccine['indications'] ]:
889 try:
890 no_of_shots4ind, latest_vacc4ind = latest_vaccs[l10n_ind]
891 ago = gmDateTime.format_interval_medically(gmDateTime.pydt_now_here() - latest_vacc4ind['date_given'])
892 lines.append(_('%s (most recent shot of %s: %s ago)') % (l10n_ind, no_of_shots4ind, ago))
893 except KeyError:
894 lines.append(_('%s (no previous vaccination recorded)') % l10n_ind)
895
896 self._TCTRL_indications.SetValue(_('Protects against:\n ') + '\n '.join(lines))
897
898 #----------------------------------------------------------------
899 # generic Edit Area mixin API
900 #----------------------------------------------------------------
902
903 has_errors = False
904
905 if not self._PRW_date_given.is_valid_timestamp(empty_is_valid = False):
906 has_errors = True
907
908 vaccine = self._PRW_vaccine.GetData(as_instance = True)
909 if vaccine is None:
910 has_errors = True
911 self._PRW_vaccine.display_as_valid(False)
912 else:
913 self._PRW_vaccine.display_as_valid(True)
914
915 if self._PRW_batch.GetValue().strip() == '':
916 has_errors = True
917 self._PRW_batch.display_as_valid(False)
918 else:
919 self._PRW_batch.display_as_valid(True)
920
921 if self._PRW_episode.GetValue().strip() == '':
922 self._PRW_episode.SetText(value = _('prevention'))
923
924 return (has_errors is False)
925
926 #----------------------------------------------------------------
928
929 vaccine = self._PRW_vaccine.GetData()
930 data = self.__save_new_from_vaccine(vaccine = vaccine)
931
932 # must be done very late or else the property access
933 # will refresh the display such that later field
934 # access will return empty values
935 self.data = data
936
937 return True
938
939 #----------------------------------------------------------------
941
942 emr = gmPerson.gmCurrentPatient().emr
943
944 data = emr.add_vaccination (
945 episode = self._PRW_episode.GetData(can_create = True, is_open = False),
946 vaccine = vaccine,
947 batch_no = self._PRW_batch.GetValue().strip()
948 )
949
950 if self._CHBOX_anamnestic.GetValue() is True:
951 data['soap_cat'] = 's'
952 else:
953 data['soap_cat'] = 'p'
954
955 data['date_given'] = self._PRW_date_given.GetData()
956 data['site'] = self._PRW_site.GetValue().strip()
957 data['pk_provider'] = self._PRW_provider.GetData()
958 data['reaction'] = self._PRW_reaction.GetValue().strip()
959 data['comment'] = self._TCTRL_comment.GetValue().strip()
960
961 data.save()
962
963 return data
964
965 #----------------------------------------------------------------
967
968 if self._CHBOX_anamnestic.GetValue() is True:
969 self.data['soap_cat'] = 's'
970 else:
971 self.data['soap_cat'] = 'p'
972
973 self.data['date_given'] = self._PRW_date_given.GetData()
974 self.data['pk_vaccine'] = self._PRW_vaccine.GetData()
975 self.data['batch_no'] = self._PRW_batch.GetValue().strip()
976 self.data['pk_episode'] = self._PRW_episode.GetData(can_create = True, is_open = False)
977 self.data['site'] = self._PRW_site.GetValue().strip()
978 self.data['pk_provider'] = self._PRW_provider.GetData()
979 self.data['reaction'] = self._PRW_reaction.GetValue().strip()
980 self.data['comment'] = self._TCTRL_comment.GetValue().strip()
981
982 self.data.save()
983
984 return True
985
986 #----------------------------------------------------------------
988 self._PRW_date_given.SetText(data = gmDateTime.pydt_now_here())
989 self._CHBOX_anamnestic.SetValue(False)
990 self._PRW_vaccine.SetText(value = '', data = None, suppress_smarts = True)
991 self._PRW_batch.unset_context(context = 'pk_vaccine')
992 self._PRW_batch.SetValue('')
993 self._PRW_episode.SetText(value = '', data = None, suppress_smarts = True)
994 self._PRW_site.SetValue('')
995 self._PRW_provider.SetData(data = None)
996 self._PRW_reaction.SetText(value = '', data = None, suppress_smarts = True)
997 self._BTN_report.Enable(False)
998 self._TCTRL_comment.SetValue('')
999
1000 self.__refresh_indications()
1001
1002 self._PRW_date_given.SetFocus()
1003
1004 #----------------------------------------------------------------
1006 self._PRW_date_given.SetText(data = self.data['date_given'])
1007 if self.data['soap_cat'] == 's':
1008 self._CHBOX_anamnestic.SetValue(True)
1009 else:
1010 self._CHBOX_anamnestic.SetValue(False)
1011 self._PRW_vaccine.SetText(value = self.data['vaccine'], data = self.data['pk_vaccine'])
1012
1013 self._PRW_batch.SetValue(self.data['batch_no'])
1014 self._PRW_episode.SetData(data = self.data['pk_episode'])
1015 self._PRW_site.SetValue(gmTools.coalesce(self.data['site'], ''))
1016 self._PRW_provider.SetData(self.data['pk_provider'])
1017 self._PRW_reaction.SetValue(gmTools.coalesce(self.data['reaction'], ''))
1018 if self.data['reaction'] is None:
1019 self._BTN_report.Enable(False)
1020 else:
1021 self._BTN_report.Enable(True)
1022 self._TCTRL_comment.SetValue(gmTools.coalesce(self.data['comment'], ''))
1023
1024 self.__refresh_indications()
1025
1026 self._PRW_date_given.SetFocus()
1027
1028 #----------------------------------------------------------------
1030 self._PRW_date_given.SetText(data = self.data['date_given'])
1031 #self._CHBOX_anamnestic.SetValue(False)
1032 self._PRW_vaccine.SetText(value = self.data['vaccine'], data = self.data['pk_vaccine'])
1033
1034 self._PRW_batch.set_context(context = 'pk_vaccine', val = self.data['pk_vaccine'])
1035 self._PRW_batch.SetValue('')
1036
1037 self._PRW_episode.SetData(data = self.data['pk_episode'])
1038 self._PRW_site.SetValue(gmTools.coalesce(self.data['site'], ''))
1039 self._PRW_provider.SetData(self.data['pk_provider'])
1040 self._PRW_reaction.SetValue('')
1041 self._BTN_report.Enable(False)
1042 self._TCTRL_comment.SetValue('')
1043
1044 self.__refresh_indications()
1045
1046 self._PRW_date_given.SetFocus()
1047
1048 #----------------------------------------------------------------
1049 # event handlers
1050 #----------------------------------------------------------------
1066
1067 #----------------------------------------------------------------
1070 # FIXME: could set newly generated vaccine here
1071
1072 #======================================================================
1073 #======================================================================
1074 #======================================================================
1075 #======================================================================
1077
1079 wx.Panel.__init__(self, parent, id, wx.DefaultPosition, wx.DefaultSize, wx.RAISED_BORDER)
1080 gmRegetMixin.cRegetOnPaintMixin.__init__(self)
1081 self.__pat = gmPerson.gmCurrentPatient()
1082 # do this here so "import cImmunisationsPanel from gmVaccWidgets" works
1083 self.ID_VaccinatedIndicationsList = wx.NewId()
1084 self.ID_VaccinationsPerRegimeList = wx.NewId()
1085 self.ID_MissingShots = wx.NewId()
1086 self.ID_ActiveSchedules = wx.NewId()
1087 self.__do_layout()
1088 self.__register_interests()
1089 self.__reset_ui_content()
1090 #----------------------------------------------------
1092 #-----------------------------------------------
1093 # top part
1094 #-----------------------------------------------
1095 pnl_UpperCaption = gmTerryGuiParts.cHeadingCaption(self, -1, _(" IMMUNISATIONS "))
1096 self.editarea = cVaccinationEditArea(self, -1, wx.DefaultPosition, wx.DefaultSize, wx.NO_BORDER)
1097
1098 #-----------------------------------------------
1099 # middle part
1100 #-----------------------------------------------
1101 # divider headings below editing area
1102 indications_heading = gmTerryGuiParts.cDividerCaption(self, -1, _("Indications"))
1103 vaccinations_heading = gmTerryGuiParts.cDividerCaption(self, -1, _("Vaccinations"))
1104 schedules_heading = gmTerryGuiParts.cDividerCaption(self, -1, _("Active Schedules"))
1105 szr_MiddleCap = wx.BoxSizer(wx.HORIZONTAL)
1106 szr_MiddleCap.Add(indications_heading, 4, wx.EXPAND)
1107 szr_MiddleCap.Add(vaccinations_heading, 6, wx.EXPAND)
1108 szr_MiddleCap.Add(schedules_heading, 10, wx.EXPAND)
1109
1110 # left list: indications for which vaccinations have been given
1111 self.LBOX_vaccinated_indications = wx.ListBox(
1112 parent = self,
1113 id = self.ID_VaccinatedIndicationsList,
1114 choices = [],
1115 style = wx.LB_HSCROLL | wx.LB_NEEDED_SB | wx.SUNKEN_BORDER
1116 )
1117 self.LBOX_vaccinated_indications.SetFont(wx.Font(12,wx.SWISS, wx.NORMAL, wx.NORMAL, False, ''))
1118
1119 # right list: when an indication has been selected on the left
1120 # display the corresponding vaccinations on the right
1121 self.LBOX_given_shots = wx.ListBox(
1122 parent = self,
1123 id = self.ID_VaccinationsPerRegimeList,
1124 choices = [],
1125 style = wx.LB_HSCROLL | wx.LB_NEEDED_SB | wx.SUNKEN_BORDER
1126 )
1127 self.LBOX_given_shots.SetFont(wx.Font(12,wx.SWISS, wx.NORMAL, wx.NORMAL, False, ''))
1128
1129 self.LBOX_active_schedules = wx.ListBox (
1130 parent = self,
1131 id = self.ID_ActiveSchedules,
1132 choices = [],
1133 style = wx.LB_HSCROLL | wx.LB_NEEDED_SB | wx.SUNKEN_BORDER
1134 )
1135 self.LBOX_active_schedules.SetFont(wx.Font(12, wx.SWISS, wx.NORMAL, wx.NORMAL, False, ''))
1136
1137 szr_MiddleLists = wx.BoxSizer(wx.HORIZONTAL)
1138 szr_MiddleLists.Add(self.LBOX_vaccinated_indications, 4, wx.EXPAND)
1139 szr_MiddleLists.Add(self.LBOX_given_shots, 6, wx.EXPAND)
1140 szr_MiddleLists.Add(self.LBOX_active_schedules, 10, wx.EXPAND)
1141
1142 #---------------------------------------------
1143 # bottom part
1144 #---------------------------------------------
1145 missing_heading = gmTerryGuiParts.cDividerCaption(self, -1, _("Missing Immunisations"))
1146 szr_BottomCap = wx.BoxSizer(wx.HORIZONTAL)
1147 szr_BottomCap.Add(missing_heading, 1, wx.EXPAND)
1148
1149 self.LBOX_missing_shots = wx.ListBox (
1150 parent = self,
1151 id = self.ID_MissingShots,
1152 choices = [],
1153 style = wx.LB_HSCROLL | wx.LB_NEEDED_SB | wx.SUNKEN_BORDER
1154 )
1155 self.LBOX_missing_shots.SetFont(wx.Font(12, wx.SWISS, wx.NORMAL, wx.NORMAL, False, ''))
1156
1157 szr_BottomLists = wx.BoxSizer(wx.HORIZONTAL)
1158 szr_BottomLists.Add(self.LBOX_missing_shots, 1, wx.EXPAND)
1159
1160 # alert caption
1161 pnl_AlertCaption = gmTerryGuiParts.cAlertCaption(self, -1, _(' Alerts '))
1162
1163 #---------------------------------------------
1164 # add all elements to the main background sizer
1165 #---------------------------------------------
1166 self.mainsizer = wx.BoxSizer(wx.VERTICAL)
1167 self.mainsizer.Add(pnl_UpperCaption, 0, wx.EXPAND)
1168 self.mainsizer.Add(self.editarea, 6, wx.EXPAND)
1169 self.mainsizer.Add(szr_MiddleCap, 0, wx.EXPAND)
1170 self.mainsizer.Add(szr_MiddleLists, 4, wx.EXPAND)
1171 self.mainsizer.Add(szr_BottomCap, 0, wx.EXPAND)
1172 self.mainsizer.Add(szr_BottomLists, 4, wx.EXPAND)
1173 self.mainsizer.Add(pnl_AlertCaption, 0, wx.EXPAND)
1174
1175 self.SetAutoLayout(True)
1176 self.SetSizer(self.mainsizer)
1177 self.mainsizer.Fit(self)
1178 #----------------------------------------------------
1180 # wxPython events
1181 wx.EVT_SIZE(self, self.OnSize)
1182 wx.EVT_LISTBOX(self, self.ID_VaccinatedIndicationsList, self._on_vaccinated_indication_selected)
1183 wx.EVT_LISTBOX_DCLICK(self, self.ID_VaccinationsPerRegimeList, self._on_given_shot_selected)
1184 wx.EVT_LISTBOX_DCLICK(self, self.ID_MissingShots, self._on_missing_shot_selected)
1185 # wx.EVT_RIGHT_UP(self.lb1, self.EvtRightButton)
1186
1187 # client internal signals
1188 gmDispatcher.connect(signal= 'post_patient_selection', receiver=self._schedule_data_reget)
1189 gmDispatcher.connect(signal= 'vaccinations_updated', receiver=self._schedule_data_reget)
1190 #----------------------------------------------------
1191 # event handlers
1192 #----------------------------------------------------
1196 #----------------------------------------------------
1198 """Paste previously given shot into edit area.
1199 """
1200 self.editarea.set_data(aVacc=event.GetClientData())
1201 #----------------------------------------------------
1204 #----------------------------------------------------
1206 """Update right hand middle list to show vaccinations given for selected indication."""
1207 ind_list = event.GetEventObject()
1208 selected_item = ind_list.GetSelection()
1209 ind = ind_list.GetClientData(selected_item)
1210 # clear list
1211 self.LBOX_given_shots.Set([])
1212 emr = self.__pat.emr
1213 shots = emr.get_vaccinations(indications = [ind])
1214 # FIXME: use Set() for entire array (but problem with client_data)
1215 for shot in shots:
1216 if shot['is_booster']:
1217 marker = 'B'
1218 else:
1219 marker = '#%s' % shot['seq_no']
1220 label = '%s - %s: %s' % (marker, shot['date'].strftime('%m/%Y'), shot['vaccine'])
1221 self.LBOX_given_shots.Append(label, shot)
1222 #----------------------------------------------------
1224 # clear edit area
1225 self.editarea.set_data()
1226 # clear lists
1227 self.LBOX_vaccinated_indications.Clear()
1228 self.LBOX_given_shots.Clear()
1229 self.LBOX_active_schedules.Clear()
1230 self.LBOX_missing_shots.Clear()
1231 #----------------------------------------------------
1233 # clear lists
1234 self.LBOX_vaccinated_indications.Clear()
1235 self.LBOX_given_shots.Clear()
1236 self.LBOX_active_schedules.Clear()
1237 self.LBOX_missing_shots.Clear()
1238
1239 emr = self.__pat.emr
1240
1241 t1 = time.time()
1242 # populate vaccinated-indications list
1243 # FIXME: consider adding virtual indication "most recent" to
1244 # FIXME: display most recent of all indications as suggested by Syan
1245 status, indications = emr.get_vaccinated_indications()
1246 # FIXME: would be faster to use Set() but can't
1247 # use Set(labels, client_data), and have to know
1248 # line position in SetClientData :-(
1249 for indication in indications:
1250 self.LBOX_vaccinated_indications.Append(indication[1], indication[0])
1251 # self.LBOX_vaccinated_indications.Set(lines)
1252 # self.LBOX_vaccinated_indications.SetClientData(data)
1253 print("vaccinated indications took", time.time()-t1, "seconds")
1254
1255 t1 = time.time()
1256 # populate active schedules list
1257 scheds = emr.get_scheduled_vaccination_regimes()
1258 if scheds is None:
1259 label = _('ERROR: cannot retrieve active vaccination schedules')
1260 self.LBOX_active_schedules.Append(label)
1261 elif len(scheds) == 0:
1262 label = _('no active vaccination schedules')
1263 self.LBOX_active_schedules.Append(label)
1264 else:
1265 for sched in scheds:
1266 label = _('%s for %s (%s shots): %s') % (sched['regime'], sched['l10n_indication'], sched['shots'], sched['comment'])
1267 self.LBOX_active_schedules.Append(label)
1268 print("active schedules took", time.time()-t1, "seconds")
1269
1270 t1 = time.time()
1271 # populate missing-shots list
1272 missing_shots = emr.get_missing_vaccinations()
1273 print("getting missing shots took", time.time()-t1, "seconds")
1274 if missing_shots is None:
1275 label = _('ERROR: cannot retrieve due/overdue vaccinations')
1276 self.LBOX_missing_shots.Append(label, None)
1277 return True
1278 # due
1279 due_template = _('%.0d weeks left: shot %s for %s in %s, due %s (%s)')
1280 overdue_template = _('overdue %.0dyrs %.0dwks: shot %s for %s in schedule "%s" (%s)')
1281 for shot in missing_shots['due']:
1282 if shot['overdue']:
1283 years, days_left = divmod(shot['amount_overdue'].days, 364.25)
1284 weeks = days_left / 7
1285 # amount_overdue, seq_no, indication, regime, vacc_comment
1286 label = overdue_template % (
1287 years,
1288 weeks,
1289 shot['seq_no'],
1290 shot['l10n_indication'],
1291 shot['regime'],
1292 shot['vacc_comment']
1293 )
1294 self.LBOX_missing_shots.Append(label, shot)
1295 else:
1296 # time_left, seq_no, regime, latest_due, vacc_comment
1297 label = due_template % (
1298 shot['time_left'].days / 7,
1299 shot['seq_no'],
1300 shot['indication'],
1301 shot['regime'],
1302 shot['latest_due'].strftime('%m/%Y'),
1303 shot['vacc_comment']
1304 )
1305 self.LBOX_missing_shots.Append(label, shot)
1306 # booster
1307 lbl_template = _('due now: booster for %s in schedule "%s" (%s)')
1308 for shot in missing_shots['boosters']:
1309 # indication, regime, vacc_comment
1310 label = lbl_template % (
1311 shot['l10n_indication'],
1312 shot['regime'],
1313 shot['vacc_comment']
1314 )
1315 self.LBOX_missing_shots.Append(label, shot)
1316 print("displaying missing shots took", time.time()-t1, "seconds")
1317
1318 return True
1319 #----------------------------------------------------
1322 # FIXME:
1323 # if has_focus:
1324 # wxCallAfter(self.__reset_ui_content)
1325 # else:
1326 # return 1
1327 #----------------------------------------------------
1330 # FIXME:
1331 # if has_focus:
1332 # wxCallAfter(self.__reset_ui_content)
1333 # else:
1334 # is_stale == True
1335 # return 1
1336 #======================================================================
1337 # main
1338 #----------------------------------------------------------------------
1339 if __name__ == "__main__":
1340
1341 if len(sys.argv) < 2:
1342 sys.exit()
1343
1344 if sys.argv[1] != 'test':
1345 sys.exit()
1346
1347 app = wx.PyWidgetTester(size = (600, 600))
1348 app.SetWidget(cXxxPhraseWheel, -1)
1349 app.MainLoop()
1350
| Home | Trees | Indices | Help |
|
|---|
| Generated by Epydoc 3.0.1 on Sat Feb 29 02:55:27 2020 | http://epydoc.sourceforge.net |