1 """Widgets dealing with patient demographics."""
2
3 __version__ = "$Revision: 1.175 $"
4 __author__ = "R.Terry, SJ Tan, I Haywood, Carlos Moro <cfmoro1976@yahoo.es>"
5 __license__ = 'GPL v2 or later (details at http://www.gnu.org)'
6
7
8 import sys
9 import sys
10 import codecs
11 import re as regex
12 import logging
13 import webbrowser
14 import os
15
16
17 import wx
18 import wx.wizard
19 import wx.lib.imagebrowser as wx_imagebrowser
20 import wx.lib.statbmp as wx_genstatbmp
21
22
23
24 if __name__ == '__main__':
25 sys.path.insert(0, '../../')
26 from Gnumed.pycommon import gmDispatcher
27 from Gnumed.pycommon import gmI18N
28 from Gnumed.pycommon import gmMatchProvider
29 from Gnumed.pycommon import gmPG2
30 from Gnumed.pycommon import gmTools
31 from Gnumed.pycommon import gmCfg
32 from Gnumed.pycommon import gmDateTime
33 from Gnumed.pycommon import gmShellAPI
34
35 from Gnumed.business import gmDemographicRecord
36 from Gnumed.business import gmPersonSearch
37 from Gnumed.business import gmSurgery
38 from Gnumed.business import gmPerson
39
40 from Gnumed.wxpython import gmPhraseWheel
41 from Gnumed.wxpython import gmRegetMixin
42 from Gnumed.wxpython import gmAuthWidgets
43 from Gnumed.wxpython import gmPersonContactWidgets
44 from Gnumed.wxpython import gmEditArea
45 from Gnumed.wxpython import gmListWidgets
46 from Gnumed.wxpython import gmDateTimeInput
47 from Gnumed.wxpython import gmDataMiningWidgets
48 from Gnumed.wxpython import gmGuiHelpers
49
50
51
52 _log = logging.getLogger('gm.ui')
53
54
55 try:
56 _('dummy-no-need-to-translate-but-make-epydoc-happy')
57 except NameError:
58 _ = lambda x:x
59
60
61
62
64 if tag_image is not None:
65 if tag_image['is_in_use']:
66 gmGuiHelpers.gm_show_info (
67 aTitle = _('Editing tag'),
68 aMessage = _(
69 'Cannot edit the image tag\n'
70 '\n'
71 ' "%s"\n'
72 '\n'
73 'because it is currently in use.\n'
74 ) % tag_image['l10n_description']
75 )
76 return False
77
78 ea = cTagImageEAPnl(parent = parent, id = -1)
79 ea.data = tag_image
80 ea.mode = gmTools.coalesce(tag_image, 'new', 'edit')
81 dlg = gmEditArea.cGenericEditAreaDlg2(parent = parent, id = -1, edit_area = ea, single_entry = single_entry)
82 dlg.SetTitle(gmTools.coalesce(tag_image, _('Adding new tag'), _('Editing tag')))
83 if dlg.ShowModal() == wx.ID_OK:
84 dlg.Destroy()
85 return True
86 dlg.Destroy()
87 return False
88
90
91 if parent is None:
92 parent = wx.GetApp().GetTopWindow()
93
94 def go_to_openclipart_org(tag_image):
95 webbrowser.open (
96 url = u'http://www.openclipart.org',
97 new = False,
98 autoraise = True
99 )
100 webbrowser.open (
101 url = u'http://www.google.com',
102 new = False,
103 autoraise = True
104 )
105 return True
106
107 def edit(tag_image=None):
108 return edit_tag_image(parent = parent, tag_image = tag_image, single_entry = (tag_image is not None))
109
110 def delete(tag):
111 if tag['is_in_use']:
112 gmDispatcher.send(signal = 'statustext', msg = _('Cannot delete this tag. It is in use.'), beep = True)
113 return False
114
115 return gmDemographicRecord.delete_tag_image(tag_image = tag['pk_tag_image'])
116
117 def refresh(lctrl):
118 tags = gmDemographicRecord.get_tag_images(order_by = u'l10n_description')
119 items = [ [
120 t['l10n_description'],
121 gmTools.bool2subst(t['is_in_use'], u'X', u''),
122 u'%s' % t['size'],
123 t['pk_tag_image']
124 ] for t in tags ]
125 lctrl.set_string_items(items)
126 lctrl.set_column_widths(widths = [wx.LIST_AUTOSIZE, wx.LIST_AUTOSIZE_USEHEADER, wx.LIST_AUTOSIZE_USEHEADER, wx.LIST_AUTOSIZE])
127 lctrl.set_data(tags)
128
129 msg = _('\nTags with images registered with GNUmed.\n')
130
131 tag = gmListWidgets.get_choices_from_list (
132 parent = parent,
133 msg = msg,
134 caption = _('Showing tags with images.'),
135 columns = [_('Tag name'), _('In use'), _('Image size'), u'#'],
136 single_selection = True,
137 new_callback = edit,
138 edit_callback = edit,
139 delete_callback = delete,
140 refresh_callback = refresh,
141 left_extra_button = (_('WWW'), _('Go to www.openclipart.org for images.'), go_to_openclipart_org)
142 )
143
144 return tag
145
146 from Gnumed.wxGladeWidgets import wxgTagImageEAPnl
147
148 -class cTagImageEAPnl(wxgTagImageEAPnl.wxgTagImageEAPnl, gmEditArea.cGenericEditAreaMixin):
149
167
168
169
171
172 valid = True
173
174 if self.mode == u'new':
175 if self.__selected_image_file is None:
176 valid = False
177 gmDispatcher.send(signal = 'statustext', msg = _('Must pick an image file for a new tag.'), beep = True)
178 self._BTN_pick_image.SetFocus()
179
180 if self.__selected_image_file is not None:
181 try:
182 open(self.__selected_image_file).close()
183 except StandardError:
184 valid = False
185 self.__selected_image_file = None
186 gmDispatcher.send(signal = 'statustext', msg = _('Cannot open the image file [%s].') % self.__selected_image_file, beep = True)
187 self._BTN_pick_image.SetFocus()
188
189 if self._TCTRL_description.GetValue().strip() == u'':
190 valid = False
191 self.display_tctrl_as_valid(self._TCTRL_description, False)
192 self._TCTRL_description.SetFocus()
193 else:
194 self.display_tctrl_as_valid(self._TCTRL_description, True)
195
196 return (valid is True)
197
216
236
238 self._TCTRL_description.SetValue(u'')
239 self._TCTRL_filename.SetValue(u'')
240 self._BMP_image.SetBitmap(bitmap = wx.EmptyBitmap(100, 100))
241
242 self.__selected_image_file = None
243
244 self._TCTRL_description.SetFocus()
245
247 self._refresh_as_new()
248
261
262
263
275
276
277 from Gnumed.wxGladeWidgets import wxgVisualSoapPresenterPnl
278
280
282 wxgVisualSoapPresenterPnl.wxgVisualSoapPresenterPnl.__init__(self, *args, **kwargs)
283 self._SZR_bitmaps = self.GetSizer()
284 self.__bitmaps = []
285
286 self.__context_popup = wx.Menu()
287
288 item = self.__context_popup.Append(-1, _('&Edit comment'))
289 self.Bind(wx.EVT_MENU, self.__edit_tag, item)
290
291 item = self.__context_popup.Append(-1, _('&Remove tag'))
292 self.Bind(wx.EVT_MENU, self.__remove_tag, item)
293
294
295
297
298 self.clear()
299
300 for tag in patient.get_tags(order_by = u'l10n_description'):
301 fname = tag.export_image2file()
302 if fname is None:
303 _log.warning('cannot export image data of tag [%s]', tag['l10n_description'])
304 continue
305 img = gmGuiHelpers.file2scaled_image(filename = fname, height = 20)
306 bmp = wx_genstatbmp.GenStaticBitmap(self, -1, img, style = wx.NO_BORDER)
307 bmp.SetToolTipString(u'%s%s' % (
308 tag['l10n_description'],
309 gmTools.coalesce(tag['comment'], u'', u'\n\n%s')
310 ))
311 bmp.tag = tag
312 bmp.Bind(wx.EVT_RIGHT_UP, self._on_bitmap_rightclicked)
313
314 self._SZR_bitmaps.Add(bmp, 0, wx.LEFT | wx.RIGHT | wx.TOP | wx.BOTTOM, 1)
315 self.__bitmaps.append(bmp)
316
317 self.GetParent().Layout()
318
320 for child_idx in range(len(self._SZR_bitmaps.GetChildren())):
321 self._SZR_bitmaps.Detach(child_idx)
322 for bmp in self.__bitmaps:
323 bmp.Destroy()
324 self.__bitmaps = []
325
326
327
335
337 if self.__current_tag is None:
338 return
339
340 msg = _('Edit the comment on tag [%s]') % self.__current_tag['l10n_description']
341 comment = wx.GetTextFromUser (
342 message = msg,
343 caption = _('Editing tag comment'),
344 default_value = gmTools.coalesce(self.__current_tag['comment'], u''),
345 parent = self
346 )
347
348 if comment == u'':
349 return
350
351 if comment.strip() == self.__current_tag['comment']:
352 return
353
354 if comment == u' ':
355 self.__current_tag['comment'] = None
356 else:
357 self.__current_tag['comment'] = comment.strip()
358
359 self.__current_tag.save()
360
361
362
364 self.__current_tag = evt.GetEventObject().tag
365 self.PopupMenu(self.__context_popup, pos = wx.DefaultPosition)
366 self.__current_tag = None
367
368
370
372
373 kwargs['message'] = _("Today's KOrganizer appointments ...")
374 kwargs['button_defs'] = [
375 {'label': _('Reload'), 'tooltip': _('Reload appointments from KOrganizer')},
376 {'label': u''},
377 {'label': u''},
378 {'label': u''},
379 {'label': u'KOrganizer', 'tooltip': _('Launch KOrganizer')}
380 ]
381 gmDataMiningWidgets.cPatientListingPnl.__init__(self, *args, **kwargs)
382
383 self.fname = os.path.expanduser(os.path.join('~', '.gnumed', 'tmp', 'korganizer2gnumed.csv'))
384 self.reload_cmd = 'konsolekalendar --view --export-type csv --export-file %s' % self.fname
385
386
390
400
402 try: os.remove(self.fname)
403 except OSError: pass
404 gmShellAPI.run_command_in_shell(command=self.reload_cmd, blocking=True)
405 try:
406 csv_file = codecs.open(self.fname , mode = 'rU', encoding = 'utf8', errors = 'replace')
407 except IOError:
408 gmDispatcher.send(signal = u'statustext', msg = _('Cannot access KOrganizer transfer file [%s]') % self.fname, beep = True)
409 return
410
411 csv_lines = gmTools.unicode_csv_reader (
412 csv_file,
413 delimiter = ','
414 )
415
416 self._LCTRL_items.set_columns ([
417 _('Place'),
418 _('Start'),
419 u'',
420 u'',
421 _('Patient'),
422 _('Comment')
423 ])
424 items = []
425 data = []
426 for line in csv_lines:
427 items.append([line[5], line[0], line[1], line[3], line[4], line[6]])
428 data.append([line[4], line[7]])
429
430 self._LCTRL_items.set_string_items(items = items)
431 self._LCTRL_items.set_column_widths()
432 self._LCTRL_items.set_data(data = data)
433 self._LCTRL_items.patient_key = 0
434
435
436
439
440
441
443
444 pat = gmPerson.gmCurrentPatient()
445 curr_jobs = pat.get_occupations()
446 if len(curr_jobs) > 0:
447 old_job = curr_jobs[0]['l10n_occupation']
448 update = curr_jobs[0]['modified_when'].strftime('%m/%Y')
449 else:
450 old_job = u''
451 update = u''
452
453 msg = _(
454 'Please enter the primary occupation of the patient.\n'
455 '\n'
456 'Currently recorded:\n'
457 '\n'
458 ' %s (last updated %s)'
459 ) % (old_job, update)
460
461 new_job = wx.GetTextFromUser (
462 message = msg,
463 caption = _('Editing primary occupation'),
464 default_value = old_job,
465 parent = None
466 )
467 if new_job.strip() == u'':
468 return
469
470 for job in curr_jobs:
471
472 if job['l10n_occupation'] != new_job:
473 pat.unlink_occupation(occupation = job['l10n_occupation'])
474
475 pat.link_occupation(occupation = new_job)
476
477
492
493
494
495
497
498 go_ahead = gmGuiHelpers.gm_show_question (
499 _('Are you sure you really, positively want\n'
500 'to disable the following person ?\n'
501 '\n'
502 ' %s %s %s\n'
503 ' born %s\n'
504 '\n'
505 '%s\n'
506 ) % (
507 identity['firstnames'],
508 identity['lastnames'],
509 identity['gender'],
510 identity['dob'],
511 gmTools.bool2subst (
512 identity.is_patient,
513 _('This patient DID receive care.'),
514 _('This person did NOT receive care.')
515 )
516 ),
517 _('Disabling person')
518 )
519 if not go_ahead:
520 return True
521
522
523 conn = gmAuthWidgets.get_dbowner_connection (
524 procedure = _('Disabling patient')
525 )
526
527 if conn is False:
528 return True
529
530 if conn is None:
531 return False
532
533
534 gmPG2.run_rw_queries(queries = [{'cmd': u"update dem.identity set deleted=True where pk=%s", 'args': [identity['pk_identity']]}])
535
536 return True
537
538
539
540
555
557
559 query = u"""
560 (select distinct firstnames, firstnames from dem.names where firstnames %(fragment_condition)s order by firstnames limit 20)
561 union
562 (select distinct name, name from dem.name_gender_map where name %(fragment_condition)s order by name limit 20)"""
563 mp = gmMatchProvider.cMatchProvider_SQL2(queries=query)
564 mp.setThresholds(3, 5, 9)
565 gmPhraseWheel.cPhraseWheel.__init__ (
566 self,
567 *args,
568 **kwargs
569 )
570 self.SetToolTipString(_("Type or select a first name (forename/Christian name/given name)."))
571 self.capitalisation_mode = gmTools.CAPS_NAMES
572 self.matcher = mp
573
575
577 query = u"""
578 (select distinct preferred, preferred from dem.names where preferred %(fragment_condition)s order by preferred limit 20)
579 union
580 (select distinct firstnames, firstnames from dem.names where firstnames %(fragment_condition)s order by firstnames limit 20)
581 union
582 (select distinct name, name from dem.name_gender_map where name %(fragment_condition)s order by name limit 20)"""
583 mp = gmMatchProvider.cMatchProvider_SQL2(queries=query)
584 mp.setThresholds(3, 5, 9)
585 gmPhraseWheel.cPhraseWheel.__init__ (
586 self,
587 *args,
588 **kwargs
589 )
590 self.SetToolTipString(_("Type or select an alias (nick name, preferred name, call name, warrior name, artist name)."))
591
592
593 self.matcher = mp
594
596
598 query = u"select distinct title, title from dem.identity where title %(fragment_condition)s"
599 mp = gmMatchProvider.cMatchProvider_SQL2(queries=query)
600 mp.setThresholds(1, 3, 9)
601 gmPhraseWheel.cPhraseWheel.__init__ (
602 self,
603 *args,
604 **kwargs
605 )
606 self.SetToolTipString(_("Type or select a title. Note that the title applies to the person, not to a particular name !"))
607 self.matcher = mp
608
610 """Let user select a gender."""
611
612 _gender_map = None
613
615
616 if cGenderSelectionPhraseWheel._gender_map is None:
617 cmd = u"""
618 select tag, l10n_label, sort_weight
619 from dem.v_gender_labels
620 order by sort_weight desc"""
621 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd}], get_col_idx=True)
622 cGenderSelectionPhraseWheel._gender_map = {}
623 for gender in rows:
624 cGenderSelectionPhraseWheel._gender_map[gender[idx['tag']]] = {
625 'data': gender[idx['tag']],
626 'field_label': gender[idx['l10n_label']],
627 'list_label': gender[idx['l10n_label']],
628 'weight': gender[idx['sort_weight']]
629 }
630
631 mp = gmMatchProvider.cMatchProvider_FixedList(aSeq = cGenderSelectionPhraseWheel._gender_map.values())
632 mp.setThresholds(1, 1, 3)
633
634 gmPhraseWheel.cPhraseWheel.__init__(self, *args, **kwargs)
635 self.selection_only = True
636 self.matcher = mp
637 self.picklist_delay = 50
638
640
642 query = u"""
643 select distinct pk, (name || coalesce(' (%s ' || issuer || ')', '')) as label
644 from dem.enum_ext_id_types
645 where name %%(fragment_condition)s
646 order by label limit 25""" % _('issued by')
647 mp = gmMatchProvider.cMatchProvider_SQL2(queries=query)
648 mp.setThresholds(1, 3, 5)
649 gmPhraseWheel.cPhraseWheel.__init__(self, *args, **kwargs)
650 self.SetToolTipString(_("Enter or select a type for the external ID."))
651 self.matcher = mp
652
667
668
669
670 from Gnumed.wxGladeWidgets import wxgExternalIDEditAreaPnl
671
673 """An edit area for editing/creating external IDs.
674
675 Does NOT act on/listen to the current patient.
676 """
692
693
694
696 if ext_id is not None:
697 self.ext_id = ext_id
698
699 if self.ext_id is not None:
700 self._PRW_type.SetText(value = self.ext_id['name'], data = self.ext_id['pk_type'])
701 self._TCTRL_value.SetValue(self.ext_id['value'])
702 self._PRW_issuer.SetText(self.ext_id['issuer'])
703 self._TCTRL_comment.SetValue(gmTools.coalesce(self.ext_id['comment'], u''))
704
705
706
707
709
710 if not self.__valid_for_save():
711 return False
712
713
714 type = regex.split(' \(%s .+\)$' % _('issued by'), self._PRW_type.GetValue().strip(), 1)[0]
715
716
717 if self.ext_id is None:
718 self.identity.add_external_id (
719 type_name = type,
720 value = self._TCTRL_value.GetValue().strip(),
721 issuer = gmTools.none_if(self._PRW_issuer.GetValue().strip(), u''),
722 comment = gmTools.none_if(self._TCTRL_comment.GetValue().strip(), u'')
723 )
724
725 else:
726 self.identity.update_external_id (
727 pk_id = self.ext_id['pk_id'],
728 type = type,
729 value = self._TCTRL_value.GetValue().strip(),
730 issuer = gmTools.none_if(self._PRW_issuer.GetValue().strip(), u''),
731 comment = gmTools.none_if(self._TCTRL_comment.GetValue().strip(), u'')
732 )
733
734 return True
735
736
737
740
742 """Set the issuer according to the selected type.
743
744 Matches are fetched from existing records in backend.
745 """
746 pk_curr_type = self._PRW_type.GetData()
747 if pk_curr_type is None:
748 return True
749 rows, idx = gmPG2.run_ro_queries(queries = [{
750 'cmd': u"select issuer from dem.enum_ext_id_types where pk = %s",
751 'args': [pk_curr_type]
752 }])
753 if len(rows) == 0:
754 return True
755 wx.CallAfter(self._PRW_issuer.SetText, rows[0][0])
756 return True
757
759
760 no_errors = True
761
762
763
764
765 if self._PRW_type.GetValue().strip() == u'':
766 self._PRW_type.SetBackgroundColour('pink')
767 self._PRW_type.SetFocus()
768 self._PRW_type.Refresh()
769 else:
770 self._PRW_type.SetBackgroundColour(wx.SystemSettings_GetColour(wx.SYS_COLOUR_WINDOW))
771 self._PRW_type.Refresh()
772
773 if self._TCTRL_value.GetValue().strip() == u'':
774 self._TCTRL_value.SetBackgroundColour('pink')
775 self._TCTRL_value.SetFocus()
776 self._TCTRL_value.Refresh()
777 no_errors = False
778 else:
779 self._TCTRL_value.SetBackgroundColour(wx.SystemSettings_GetColour(wx.SYS_COLOUR_WINDOW))
780 self._TCTRL_value.Refresh()
781
782 return no_errors
783
784 from Gnumed.wxGladeWidgets import wxgIdentityEAPnl
785
786 -class cIdentityEAPnl(wxgIdentityEAPnl.wxgIdentityEAPnl, gmEditArea.cGenericEditAreaMixin):
787 """An edit area for editing/creating title/gender/dob/dod etc."""
788
804
805
806
807
808
809
810
811
813
814 has_error = False
815
816 if self._PRW_gender.GetData() is None:
817 self._PRW_gender.SetFocus()
818 has_error = True
819
820 if not self._PRW_dob.is_valid_timestamp():
821 val = self._PRW_dob.GetValue().strip()
822 gmDispatcher.send(signal = u'statustext', msg = _('Cannot parse <%s> into proper timestamp.') % val)
823 self._PRW_dob.SetBackgroundColour('pink')
824 self._PRW_dob.Refresh()
825 self._PRW_dob.SetFocus()
826 has_error = True
827 else:
828 self._PRW_dob.SetBackgroundColour(wx.SystemSettings_GetColour(wx.SYS_COLOUR_WINDOW))
829 self._PRW_dob.Refresh()
830
831 if not self._PRW_dod.is_valid_timestamp(allow_empty = True):
832 gmDispatcher.send(signal = u'statustext', msg = _('Invalid date of death.'))
833 self._PRW_dod.SetFocus()
834 has_error = True
835
836 return (has_error is False)
837
841
857
860
884
887
888 from Gnumed.wxGladeWidgets import wxgNameGenderDOBEditAreaPnl
889
891 """An edit area for editing/creating name/gender/dob.
892
893 Does NOT act on/listen to the current patient.
894 """
905
906
907
932
933
934
935
937
938 if not self.__valid_for_save():
939 return False
940
941 self.__identity['gender'] = self._PRW_gender.GetData()
942 if self._PRW_dob.GetValue().strip() == u'':
943 self.__identity['dob'] = None
944 else:
945 self.__identity['dob'] = self._PRW_dob.GetData().get_pydt()
946 self.__identity['title'] = gmTools.none_if(self._PRW_title.GetValue().strip(), u'')
947 self.__identity['deceased'] = self._PRW_dod.GetData()
948 self.__identity.save_payload()
949
950 active = self._CHBOX_active.GetValue()
951 first = self._PRW_firstname.GetValue().strip()
952 last = self._PRW_lastname.GetValue().strip()
953 old_nick = self.__name['preferred']
954
955
956 old_name = self.__name['firstnames'] + self.__name['lastnames']
957 if (first + last) != old_name:
958 self.__name = self.__identity.add_name(first, last, active)
959
960 self.__name['active_name'] = active
961 self.__name['preferred'] = gmTools.none_if(self._PRW_nick.GetValue().strip(), u'')
962 self.__name['comment'] = gmTools.none_if(self._TCTRL_comment.GetValue().strip(), u'')
963
964 self.__name.save_payload()
965
966 return True
967
968
969
972
974 """Set the gender according to entered firstname.
975
976 Matches are fetched from existing records in backend.
977 """
978 firstname = self._PRW_firstname.GetValue().strip()
979 if firstname == u'':
980 return True
981 rows, idx = gmPG2.run_ro_queries(queries = [{
982 'cmd': u"select gender from dem.name_gender_map where name ilike %s",
983 'args': [firstname]
984 }])
985 if len(rows) == 0:
986 return True
987 wx.CallAfter(self._PRW_gender.SetData, rows[0][0])
988 return True
989
990
991
1035
1036
1037
1039 """A list for managing a person's names.
1040
1041 Does NOT act on/listen to the current patient.
1042 """
1060
1061
1062
1063 - def refresh(self, *args, **kwargs):
1080
1081
1082
1084 self._LCTRL_items.set_columns(columns = [
1085 _('Active'),
1086 _('Lastname'),
1087 _('Firstname(s)'),
1088 _('Preferred Name'),
1089 _('Comment')
1090 ])
1091 self._BTN_edit.SetLabel(_('Clone and &edit'))
1092
1102
1112
1114
1115 if len(self.__identity.get_names()) == 1:
1116 gmDispatcher.send(signal = u'statustext', msg = _('Cannot delete the only name of a person.'), beep = True)
1117 return False
1118
1119 go_ahead = gmGuiHelpers.gm_show_question (
1120 _( 'It is often advisable to keep old names around and\n'
1121 'just create a new "currently active" name.\n'
1122 '\n'
1123 'This allows finding the patient by both the old\n'
1124 'and the new name (think before/after marriage).\n'
1125 '\n'
1126 'Do you still want to really delete\n'
1127 "this name from the patient ?"
1128 ),
1129 _('Deleting name')
1130 )
1131 if not go_ahead:
1132 return False
1133
1134 self.__identity.delete_name(name = name)
1135 return True
1136
1137
1138
1140 return self.__identity
1141
1145
1146 identity = property(_get_identity, _set_identity)
1147
1149 """A list for managing a person's external IDs.
1150
1151 Does NOT act on/listen to the current patient.
1152 """
1170
1171
1172
1173 - def refresh(self, *args, **kwargs):
1190
1191
1192
1194 self._LCTRL_items.set_columns(columns = [
1195 _('ID type'),
1196 _('Value'),
1197 _('Issuer'),
1198 _('Comment')
1199 ])
1200
1211
1222
1224 go_ahead = gmGuiHelpers.gm_show_question (
1225 _( 'Do you really want to delete this\n'
1226 'external ID from the patient ?'),
1227 _('Deleting external ID')
1228 )
1229 if not go_ahead:
1230 return False
1231 self.__identity.delete_external_id(pk_ext_id = ext_id['pk_id'])
1232 return True
1233
1234
1235
1237 return self.__identity
1238
1242
1243 identity = property(_get_identity, _set_identity)
1244
1245
1246
1247 from Gnumed.wxGladeWidgets import wxgPersonIdentityManagerPnl
1248
1250 """A panel for editing identity data for a person.
1251
1252 - provides access to:
1253 - name
1254 - external IDs
1255
1256 Does NOT act on/listen to the current patient.
1257 """
1264
1265
1266
1268 self._PNL_names.identity = self.__identity
1269 self._PNL_ids.identity = self.__identity
1270
1271 self._PNL_identity.mode = 'new'
1272 self._PNL_identity.data = self.__identity
1273 if self.__identity is not None:
1274 self._PNL_identity.mode = 'edit'
1275
1276
1277
1279 return self.__identity
1280
1284
1285 identity = property(_get_identity, _set_identity)
1286
1287
1288
1292
1295
1296
1297 from Gnumed.wxGladeWidgets import wxgPersonSocialNetworkManagerPnl
1298
1307
1308
1309
1311
1312 tt = _('Link another person in this database as the emergency contact:\n\nEnter person name part or identifier and hit <enter>.')
1313
1314 if self.__identity is None:
1315 self._TCTRL_er_contact.SetValue(u'')
1316 self._TCTRL_person.person = None
1317 self._TCTRL_person.SetToolTipString(tt)
1318
1319 self._PRW_provider.SetText(value = u'', data = None)
1320 return
1321
1322 self._TCTRL_er_contact.SetValue(gmTools.coalesce(self.__identity['emergency_contact'], u''))
1323 if self.__identity['pk_emergency_contact'] is not None:
1324 ident = gmPerson.cIdentity(aPK_obj = self.__identity['pk_emergency_contact'])
1325 self._TCTRL_person.person = ident
1326 tt = u'%s\n\n%s\n\n%s' % (
1327 tt,
1328 ident['description_gender'],
1329 u'\n'.join([
1330 u'%s: %s%s' % (
1331 c['l10n_comm_type'],
1332 c['url'],
1333 gmTools.bool2subst(c['is_confidential'], _(' (confidential !)'), u'', u'')
1334 )
1335 for c in ident.get_comm_channels()
1336 ])
1337 )
1338 else:
1339 self._TCTRL_person.person = None
1340
1341 self._TCTRL_person.SetToolTipString(tt)
1342
1343 if self.__identity['pk_primary_provider'] is None:
1344 self._PRW_provider.SetText(value = u'', data = None)
1345 else:
1346 self._PRW_provider.SetData(data = self.__identity['pk_primary_provider'])
1347
1348
1349
1351 return self.__identity
1352
1356
1357 identity = property(_get_identity, _set_identity)
1358
1359
1360
1374
1385
1393
1394
1395
1397
1398 dbcfg = gmCfg.cCfgSQL()
1399
1400 def_region = dbcfg.get2 (
1401 option = u'person.create.default_region',
1402 workplace = gmSurgery.gmCurrentPractice().active_workplace,
1403 bias = u'user'
1404 )
1405 def_country = None
1406
1407 if def_region is None:
1408 def_country = dbcfg.get2 (
1409 option = u'person.create.default_country',
1410 workplace = gmSurgery.gmCurrentPractice().active_workplace,
1411 bias = u'user'
1412 )
1413 else:
1414 countries = gmDemographicRecord.get_country_for_region(region = def_region)
1415 if len(countries) == 1:
1416 def_country = countries[0]['l10n_country']
1417
1418 if parent is None:
1419 parent = wx.GetApp().GetTopWindow()
1420
1421 ea = cNewPatientEAPnl(parent = parent, id = -1, country = def_country, region = def_region)
1422 dlg = gmEditArea.cGenericEditAreaDlg2(parent = parent, id = -1, edit_area = ea, single_entry = True)
1423 dlg.SetTitle(_('Adding new person'))
1424 ea._PRW_lastname.SetFocus()
1425 result = dlg.ShowModal()
1426 pat = ea.data
1427 dlg.Destroy()
1428
1429 if result != wx.ID_OK:
1430 return False
1431
1432 _log.debug('created new person [%s]', pat.ID)
1433
1434 if activate:
1435 from Gnumed.wxpython import gmPatSearchWidgets
1436 gmPatSearchWidgets.set_active_patient(patient = pat)
1437
1438 gmDispatcher.send(signal = 'display_widget', name = 'gmNotebookedPatientEditionPlugin')
1439
1440 return True
1441
1442 from Gnumed.wxGladeWidgets import wxgNewPatientEAPnl
1443
1444 -class cNewPatientEAPnl(wxgNewPatientEAPnl.wxgNewPatientEAPnl, gmEditArea.cGenericEditAreaMixin):
1445
1447
1448 try:
1449 self.default_region = kwargs['region']
1450 del kwargs['region']
1451 except KeyError:
1452 self.default_region = None
1453
1454 try:
1455 self.default_country = kwargs['country']
1456 del kwargs['country']
1457 except KeyError:
1458 self.default_country = None
1459
1460 wxgNewPatientEAPnl.wxgNewPatientEAPnl.__init__(self, *args, **kwargs)
1461 gmEditArea.cGenericEditAreaMixin.__init__(self)
1462
1463 self.mode = 'new'
1464 self.data = None
1465 self._address = None
1466
1467 self.__init_ui()
1468 self.__register_interests()
1469
1470
1471
1473 self._PRW_lastname.final_regex = '.+'
1474 self._PRW_firstnames.final_regex = '.+'
1475 self._PRW_address_searcher.selection_only = False
1476
1477
1478
1479
1480 if self.default_country is not None:
1481 self._PRW_country.SetText(value = self.default_country)
1482
1483 if self.default_region is not None:
1484 self._PRW_region.SetText(value = self.default_region)
1485
1487
1488 adr = self._PRW_address_searcher.get_address()
1489 if adr is None:
1490 return True
1491
1492 if ctrl.GetValue().strip() != adr[field]:
1493 wx.CallAfter(self._PRW_address_searcher.SetText, value = u'', data = None)
1494 return True
1495
1496 return False
1497
1499 adr = self._PRW_address_searcher.get_address()
1500 if adr is None:
1501 return True
1502
1503 self._PRW_zip.SetText(value = adr['postcode'], data = adr['postcode'])
1504
1505 self._PRW_street.SetText(value = adr['street'], data = adr['street'])
1506 self._PRW_street.set_context(context = u'zip', val = adr['postcode'])
1507
1508 self._TCTRL_number.SetValue(adr['number'])
1509
1510 self._PRW_urb.SetText(value = adr['urb'], data = adr['urb'])
1511 self._PRW_urb.set_context(context = u'zip', val = adr['postcode'])
1512
1513 self._PRW_region.SetText(value = adr['l10n_state'], data = adr['code_state'])
1514 self._PRW_region.set_context(context = u'zip', val = adr['postcode'])
1515
1516 self._PRW_country.SetText(value = adr['l10n_country'], data = adr['code_country'])
1517 self._PRW_country.set_context(context = u'zip', val = adr['postcode'])
1518
1520 error = False
1521
1522
1523 if self._PRW_lastname.GetValue().strip() == u'':
1524 error = True
1525 gmDispatcher.send(signal = 'statustext', msg = _('Must enter lastname.'))
1526 self._PRW_lastname.display_as_valid(False)
1527 else:
1528 self._PRW_lastname.display_as_valid(True)
1529
1530 if self._PRW_firstnames.GetValue().strip() == '':
1531 error = True
1532 gmDispatcher.send(signal = 'statustext', msg = _('Must enter first name.'))
1533 self._PRW_firstnames.display_as_valid(False)
1534 else:
1535 self._PRW_firstnames.display_as_valid(True)
1536
1537
1538 if self._PRW_gender.GetData() is None:
1539 error = True
1540 gmDispatcher.send(signal = 'statustext', msg = _('Must select gender.'))
1541 self._PRW_gender.display_as_valid(False)
1542 else:
1543 self._PRW_gender.display_as_valid(True)
1544
1545
1546
1547
1548 if self._PRW_dob.is_valid_timestamp(allow_empty = False):
1549 dob = self._PRW_dob.date
1550
1551 msg = None
1552 if dob.year < 1900:
1553 msg = _(
1554 'DOB: %s\n'
1555 '\n'
1556 'While this is a valid point in time Python does\n'
1557 'not know how to deal with it.\n'
1558 '\n'
1559 'We suggest using January 1st 1901 instead and adding\n'
1560 'the true date of birth to the patient comment.\n'
1561 '\n'
1562 'Sorry for the inconvenience %s'
1563 ) % (dob, gmTools.u_frowning_face)
1564 elif dob > gmDateTime.pydt_now_here():
1565 msg = _(
1566 'DOB: %s\n'
1567 '\n'
1568 'Date of birth in the future !'
1569 ) % dob
1570
1571 if msg is not None:
1572 error = True
1573 gmGuiHelpers.gm_show_error (
1574 msg,
1575 _('Registering new person')
1576 )
1577 self._PRW_dob.display_as_valid(False)
1578 self._PRW_dob.SetFocus()
1579
1580 else:
1581
1582 if error is False:
1583
1584 if self._PRW_dob.GetValue().strip() == u'':
1585
1586 allow_empty_dob = gmGuiHelpers.gm_show_question (
1587 _(
1588 'Are you sure you want to register this person\n'
1589 'without a valid date of birth ?\n'
1590 '\n'
1591 'This can be useful for temporary staff members\n'
1592 'but will provoke nag screens if this person\n'
1593 'becomes a patient.\n'
1594 ),
1595 _('Registering new person')
1596 )
1597 if allow_empty_dob:
1598 self._PRW_dob.display_as_valid(True)
1599 else:
1600 error = True
1601 self._PRW_dob.SetFocus()
1602
1603
1604
1605
1606 return (not error)
1607
1609
1610
1611 if self._PRW_address_searcher.GetData() is not None:
1612 wx.CallAfter(self.__set_fields_from_address_searcher)
1613 return True
1614
1615
1616 fields_to_fill = (
1617 self._TCTRL_number,
1618 self._PRW_zip,
1619 self._PRW_street,
1620 self._PRW_urb,
1621 self._PRW_region,
1622 self._PRW_country
1623 )
1624 no_of_filled_fields = 0
1625
1626 for field in fields_to_fill:
1627 if field.GetValue().strip() != u'':
1628 no_of_filled_fields += 1
1629 field.SetBackgroundColour(gmPhraseWheel.color_prw_valid)
1630 field.Refresh()
1631
1632
1633 if no_of_filled_fields == 0:
1634 if empty_address_is_valid:
1635 return True
1636 else:
1637 return None
1638
1639
1640 if no_of_filled_fields != len(fields_to_fill):
1641 for field in fields_to_fill:
1642 if field.GetValue().strip() == u'':
1643 field.SetBackgroundColour(gmPhraseWheel.color_prw_invalid)
1644 field.SetFocus()
1645 field.Refresh()
1646 msg = _('To properly create an address, all the related fields must be filled in.')
1647 gmGuiHelpers.gm_show_error(msg, _('Required fields'))
1648 return False
1649
1650
1651
1652
1653 strict_fields = (
1654 self._PRW_region,
1655 self._PRW_country
1656 )
1657 error = False
1658 for field in strict_fields:
1659 if field.GetData() is None:
1660 error = True
1661 field.SetBackgroundColour(gmPhraseWheel.color_prw_invalid)
1662 field.SetFocus()
1663 else:
1664 field.SetBackgroundColour(gmPhraseWheel.color_prw_valid)
1665 field.Refresh()
1666
1667 if error:
1668 msg = _('This field must contain an item selected from the dropdown list.')
1669 gmGuiHelpers.gm_show_error(msg, _('Required fields'))
1670 return False
1671
1672 return True
1673
1690
1691
1692
1694 """Set the gender according to entered firstname.
1695
1696 Matches are fetched from existing records in backend.
1697 """
1698
1699
1700 if self._PRW_gender.GetData() is not None:
1701 return True
1702
1703 firstname = self._PRW_firstnames.GetValue().strip()
1704 if firstname == u'':
1705 return True
1706
1707 gender = gmPerson.map_firstnames2gender(firstnames = firstname)
1708 if gender is None:
1709 return True
1710
1711 wx.CallAfter(self._PRW_gender.SetData, gender)
1712 return True
1713
1715 self.__perhaps_invalidate_address_searcher(self._PRW_zip, 'postcode')
1716
1717 zip_code = gmTools.none_if(self._PRW_zip.GetValue().strip(), u'')
1718 self._PRW_street.set_context(context = u'zip', val = zip_code)
1719 self._PRW_urb.set_context(context = u'zip', val = zip_code)
1720 self._PRW_region.set_context(context = u'zip', val = zip_code)
1721 self._PRW_country.set_context(context = u'zip', val = zip_code)
1722
1723 return True
1724
1726 self.__perhaps_invalidate_address_searcher(self._PRW_country, 'l10n_country')
1727
1728 country = gmTools.none_if(self._PRW_country.GetValue().strip(), u'')
1729 self._PRW_region.set_context(context = u'country', val = country)
1730
1731 return True
1732
1734 mapping = [
1735 (self._PRW_street, 'street'),
1736 (self._TCTRL_number, 'number'),
1737 (self._PRW_urb, 'urb'),
1738 (self._PRW_region, 'l10n_state')
1739 ]
1740
1741
1742 for ctrl, field in mapping:
1743 if self.__perhaps_invalidate_address_searcher(ctrl, field):
1744 return True
1745
1746 return True
1747
1749 adr = self._PRW_address_searcher.get_address()
1750 if adr is None:
1751 return True
1752
1753 wx.CallAfter(self.__set_fields_from_address_searcher)
1754 return True
1755
1756
1757
1759 return (self.__identity_valid_for_save() and self.__address_valid_for_save(empty_address_is_valid = True))
1760
1762
1763
1764 new_identity = gmPerson.create_identity (
1765 gender = self._PRW_gender.GetData(),
1766 dob = self._PRW_dob.GetData(),
1767 lastnames = self._PRW_lastname.GetValue().strip(),
1768 firstnames = self._PRW_firstnames.GetValue().strip()
1769 )
1770 _log.debug('identity created: %s' % new_identity)
1771
1772 new_identity['title'] = gmTools.none_if(self._PRW_title.GetValue().strip())
1773 new_identity.set_nickname(nickname = gmTools.none_if(self._PRW_nickname.GetValue().strip(), u''))
1774
1775 new_identity.save()
1776
1777 name = new_identity.get_active_name()
1778 name['comment'] = gmTools.none_if(self._TCTRL_comment.GetValue().strip(), u'')
1779 name.save()
1780
1781
1782 is_valid = self.__address_valid_for_save(empty_address_is_valid = False)
1783 if is_valid is True:
1784
1785
1786 try:
1787 new_identity.link_address (
1788 number = self._TCTRL_number.GetValue().strip(),
1789 street = self._PRW_street.GetValue().strip(),
1790 postcode = self._PRW_zip.GetValue().strip(),
1791 urb = self._PRW_urb.GetValue().strip(),
1792 state = self._PRW_region.GetData(),
1793 country = self._PRW_country.GetData()
1794 )
1795 except gmPG2.dbapi.InternalError:
1796 _log.debug('number: >>%s<<', self._TCTRL_number.GetValue().strip())
1797 _log.debug('street: >>%s<<', self._PRW_street.GetValue().strip())
1798 _log.debug('postcode: >>%s<<', self._PRW_zip.GetValue().strip())
1799 _log.debug('urb: >>%s<<', self._PRW_urb.GetValue().strip())
1800 _log.debug('state: >>%s<<', self._PRW_region.GetData().strip())
1801 _log.debug('country: >>%s<<', self._PRW_country.GetData().strip())
1802 _log.exception('cannot link address')
1803 gmGuiHelpers.gm_show_error (
1804 aTitle = _('Saving address'),
1805 aMessage = _(
1806 'Cannot save this address.\n'
1807 '\n'
1808 'You will have to add it via the Demographics plugin.\n'
1809 )
1810 )
1811 elif is_valid is False:
1812 gmGuiHelpers.gm_show_error (
1813 aTitle = _('Saving address'),
1814 aMessage = _(
1815 'Address not saved.\n'
1816 '\n'
1817 'You will have to add it via the Demographics plugin.\n'
1818 )
1819 )
1820
1821
1822
1823 channel_name = self._PRW_channel_type.GetValue().strip()
1824 pk_channel_type = self._PRW_channel_type.GetData()
1825 if pk_channel_type is None:
1826 if channel_name == u'':
1827 channel_name = u'homephone'
1828 new_identity.link_comm_channel (
1829 comm_medium = channel_name,
1830 pk_channel_type = pk_channel_type,
1831 url = gmTools.none_if(self._TCTRL_phone.GetValue().strip(), u''),
1832 is_confidential = False
1833 )
1834
1835
1836 pk_type = self._PRW_external_id_type.GetData()
1837 id_value = self._TCTRL_external_id_value.GetValue().strip()
1838 if (pk_type is not None) and (id_value != u''):
1839 new_identity.add_external_id(value = id_value, pk_type = pk_type)
1840
1841
1842 new_identity.link_occupation (
1843 occupation = gmTools.none_if(self._PRW_occupation.GetValue().strip(), u'')
1844 )
1845
1846 self.data = new_identity
1847 return True
1848
1850 raise NotImplementedError('[%s]: not expected to be used' % self.__class__.__name__)
1851
1855
1858
1860 raise NotImplementedError('[%s]: not expected to be used' % self.__class__.__name__)
1861
1862
1863
1864
1866 """Notebook displaying demographics editing pages:
1867
1868 - Identity
1869 - Contacts (addresses, phone numbers, etc)
1870 - Social network (significant others, GP, etc)
1871
1872 Does NOT act on/listen to the current patient.
1873 """
1874
1876
1877 wx.Notebook.__init__ (
1878 self,
1879 parent = parent,
1880 id = id,
1881 style = wx.NB_TOP | wx.NB_MULTILINE | wx.NO_BORDER,
1882 name = self.__class__.__name__
1883 )
1884
1885 self.__identity = None
1886 self.__do_layout()
1887 self.SetSelection(0)
1888
1889
1890
1892 """Populate fields in pages with data from model."""
1893 for page_idx in range(self.GetPageCount()):
1894 page = self.GetPage(page_idx)
1895 page.identity = self.__identity
1896
1897 return True
1898
1899
1900
1930
1931
1932
1934 return self.__identity
1935
1938
1939 identity = property(_get_identity, _set_identity)
1940
1941
1942
1943
1944
1945
1947 """Page containing patient occupations edition fields.
1948 """
1949 - def __init__(self, parent, id, ident=None):
1950 """
1951 Creates a new instance of BasicPatDetailsPage
1952 @param parent - The parent widget
1953 @type parent - A wx.Window instance
1954 @param id - The widget id
1955 @type id - An integer
1956 """
1957 wx.Panel.__init__(self, parent, id)
1958 self.__ident = ident
1959 self.__do_layout()
1960
1962 PNL_form = wx.Panel(self, -1)
1963
1964 STT_occupation = wx.StaticText(PNL_form, -1, _('Occupation'))
1965 self.PRW_occupation = cOccupationPhraseWheel(parent = PNL_form, id = -1)
1966 self.PRW_occupation.SetToolTipString(_("primary occupation of the patient"))
1967
1968 STT_occupation_updated = wx.StaticText(PNL_form, -1, _('Last updated'))
1969 self.TTC_occupation_updated = wx.TextCtrl(PNL_form, -1, style = wx.TE_READONLY)
1970
1971
1972 SZR_input = wx.FlexGridSizer(cols = 2, rows = 5, vgap = 4, hgap = 4)
1973 SZR_input.AddGrowableCol(1)
1974 SZR_input.Add(STT_occupation, 0, wx.SHAPED)
1975 SZR_input.Add(self.PRW_occupation, 1, wx.EXPAND)
1976 SZR_input.Add(STT_occupation_updated, 0, wx.SHAPED)
1977 SZR_input.Add(self.TTC_occupation_updated, 1, wx.EXPAND)
1978 PNL_form.SetSizerAndFit(SZR_input)
1979
1980
1981 SZR_main = wx.BoxSizer(wx.VERTICAL)
1982 SZR_main.Add(PNL_form, 1, wx.EXPAND)
1983 self.SetSizer(SZR_main)
1984
1987
1988 - def refresh(self, identity=None):
1989 if identity is not None:
1990 self.__ident = identity
1991 jobs = self.__ident.get_occupations()
1992 if len(jobs) > 0:
1993 self.PRW_occupation.SetText(jobs[0]['l10n_occupation'])
1994 self.TTC_occupation_updated.SetValue(jobs[0]['modified_when'].strftime('%m/%Y'))
1995 return True
1996
1998 if self.PRW_occupation.IsModified():
1999 new_job = self.PRW_occupation.GetValue().strip()
2000 jobs = self.__ident.get_occupations()
2001 for job in jobs:
2002 if job['l10n_occupation'] == new_job:
2003 continue
2004 self.__ident.unlink_occupation(occupation = job['l10n_occupation'])
2005 self.__ident.link_occupation(occupation = new_job)
2006 return True
2007
2009 """Patient demographics plugin for main notebook.
2010
2011 Hosts another notebook with pages for Identity, Contacts, etc.
2012
2013 Acts on/listens to the currently active patient.
2014 """
2015
2021
2022
2023
2024
2025
2026
2028 """Arrange widgets."""
2029 self.__patient_notebook = cPersonDemographicsEditorNb(self, -1)
2030
2031 szr_main = wx.BoxSizer(wx.VERTICAL)
2032 szr_main.Add(self.__patient_notebook, 1, wx.EXPAND)
2033 self.SetSizerAndFit(szr_main)
2034
2035
2036
2038 gmDispatcher.connect(signal = u'pre_patient_selection', receiver = self._on_pre_patient_selection)
2039 gmDispatcher.connect(signal = u'post_patient_selection', receiver = self._on_post_patient_selection)
2040
2042 self._schedule_data_reget()
2043
2045 self._schedule_data_reget()
2046
2047
2048
2058
2059
2060
2061
2062
2063
2064
2065
2066
2067 -class cBasicPatDetailsPage(wx.wizard.WizardPageSimple):
2068 """
2069 Wizard page for entering patient's basic demographic information
2070 """
2071
2072 form_fields = (
2073 'firstnames', 'lastnames', 'nick', 'dob', 'gender', 'title', 'occupation',
2074 'address_number', 'zip_code', 'street', 'town', 'state', 'country', 'phone', 'comment'
2075 )
2076
2077 - def __init__(self, parent, title):
2078 """
2079 Creates a new instance of BasicPatDetailsPage
2080 @param parent - The parent widget
2081 @type parent - A wx.Window instance
2082 @param tile - The title of the page
2083 @type title - A StringType instance
2084 """
2085 wx.wizard.WizardPageSimple.__init__(self, parent)
2086 self.__title = title
2087 self.__do_layout()
2088 self.__register_interests()
2089
2090 - def __do_layout(self):
2091 PNL_form = wx.Panel(self, -1)
2092
2093
2094 STT_lastname = wx.StaticText(PNL_form, -1, _('Last name'))
2095 STT_lastname.SetForegroundColour('red')
2096 self.PRW_lastname = cLastnamePhraseWheel(parent = PNL_form, id = -1)
2097 self.PRW_lastname.SetToolTipString(_('Required: lastname (family name)'))
2098
2099
2100 STT_firstname = wx.StaticText(PNL_form, -1, _('First name(s)'))
2101 STT_firstname.SetForegroundColour('red')
2102 self.PRW_firstname = cFirstnamePhraseWheel(parent = PNL_form, id = -1)
2103 self.PRW_firstname.SetToolTipString(_('Required: surname/given name/first name'))
2104
2105
2106 STT_nick = wx.StaticText(PNL_form, -1, _('Nick name'))
2107 self.PRW_nick = cNicknamePhraseWheel(parent = PNL_form, id = -1)
2108
2109
2110 STT_dob = wx.StaticText(PNL_form, -1, _('Date of birth'))
2111 STT_dob.SetForegroundColour('red')
2112 self.PRW_dob = gmDateTimeInput.cFuzzyTimestampInput(parent = PNL_form, id = -1)
2113 self.PRW_dob.SetToolTipString(_("Required: date of birth, if unknown or aliasing wanted then invent one"))
2114
2115
2116 STT_gender = wx.StaticText(PNL_form, -1, _('Gender'))
2117 STT_gender.SetForegroundColour('red')
2118 self.PRW_gender = cGenderSelectionPhraseWheel(parent = PNL_form, id=-1)
2119 self.PRW_gender.SetToolTipString(_("Required: gender of patient"))
2120
2121
2122 STT_title = wx.StaticText(PNL_form, -1, _('Title'))
2123 self.PRW_title = cTitlePhraseWheel(parent = PNL_form, id = -1)
2124
2125
2126 STT_zip_code = wx.StaticText(PNL_form, -1, _('Postal code'))
2127 STT_zip_code.SetForegroundColour('orange')
2128 self.PRW_zip_code = gmPersonContactWidgets.cZipcodePhraseWheel(parent = PNL_form, id = -1)
2129 self.PRW_zip_code.SetToolTipString(_("primary/home address: zip/postal code"))
2130
2131
2132 STT_street = wx.StaticText(PNL_form, -1, _('Street'))
2133 STT_street.SetForegroundColour('orange')
2134 self.PRW_street = gmPersonContactWidgets.cStreetPhraseWheel(parent = PNL_form, id = -1)
2135 self.PRW_street.SetToolTipString(_("primary/home address: name of street"))
2136
2137
2138 STT_address_number = wx.StaticText(PNL_form, -1, _('Number'))
2139 STT_address_number.SetForegroundColour('orange')
2140 self.TTC_address_number = wx.TextCtrl(PNL_form, -1)
2141 self.TTC_address_number.SetToolTipString(_("primary/home address: address number"))
2142
2143
2144 STT_town = wx.StaticText(PNL_form, -1, _('Place'))
2145 STT_town.SetForegroundColour('orange')
2146 self.PRW_town = gmPersonContactWidgets.cUrbPhraseWheel(parent = PNL_form, id = -1)
2147 self.PRW_town.SetToolTipString(_("primary/home address: city/town/village/dwelling/..."))
2148
2149
2150 STT_state = wx.StaticText(PNL_form, -1, _('Region'))
2151 STT_state.SetForegroundColour('orange')
2152 self.PRW_state = gmPersonContactWidgets.cStateSelectionPhraseWheel(parent=PNL_form, id=-1)
2153 self.PRW_state.SetToolTipString(_("primary/home address: state/province/county/..."))
2154
2155
2156 STT_country = wx.StaticText(PNL_form, -1, _('Country'))
2157 STT_country.SetForegroundColour('orange')
2158 self.PRW_country = gmPersonContactWidgets.cCountryPhraseWheel(parent = PNL_form, id = -1)
2159 self.PRW_country.SetToolTipString(_("primary/home address: country"))
2160
2161
2162 STT_phone = wx.StaticText(PNL_form, -1, _('Phone'))
2163 self.TTC_phone = wx.TextCtrl(PNL_form, -1)
2164 self.TTC_phone.SetToolTipString(_("phone number at home"))
2165
2166
2167 STT_occupation = wx.StaticText(PNL_form, -1, _('Occupation'))
2168 self.PRW_occupation = cOccupationPhraseWheel(parent = PNL_form, id = -1)
2169
2170
2171 STT_comment = wx.StaticText(PNL_form, -1, _('Comment'))
2172 self.TCTRL_comment = wx.TextCtrl(PNL_form, -1)
2173 self.TCTRL_comment.SetToolTipString(_('A comment on this patient.'))
2174
2175
2176 self.form_DTD = cFormDTD(fields = self.__class__.form_fields)
2177 PNL_form.SetValidator(cBasicPatDetailsPageValidator(dtd = self.form_DTD))
2178
2179
2180 SZR_input = wx.FlexGridSizer(cols = 2, rows = 16, vgap = 4, hgap = 4)
2181 SZR_input.AddGrowableCol(1)
2182 SZR_input.Add(STT_lastname, 0, wx.SHAPED)
2183 SZR_input.Add(self.PRW_lastname, 1, wx.EXPAND)
2184 SZR_input.Add(STT_firstname, 0, wx.SHAPED)
2185 SZR_input.Add(self.PRW_firstname, 1, wx.EXPAND)
2186 SZR_input.Add(STT_nick, 0, wx.SHAPED)
2187 SZR_input.Add(self.PRW_nick, 1, wx.EXPAND)
2188 SZR_input.Add(STT_dob, 0, wx.SHAPED)
2189 SZR_input.Add(self.PRW_dob, 1, wx.EXPAND)
2190 SZR_input.Add(STT_gender, 0, wx.SHAPED)
2191 SZR_input.Add(self.PRW_gender, 1, wx.EXPAND)
2192 SZR_input.Add(STT_title, 0, wx.SHAPED)
2193 SZR_input.Add(self.PRW_title, 1, wx.EXPAND)
2194 SZR_input.Add(STT_zip_code, 0, wx.SHAPED)
2195 SZR_input.Add(self.PRW_zip_code, 1, wx.EXPAND)
2196 SZR_input.Add(STT_street, 0, wx.SHAPED)
2197 SZR_input.Add(self.PRW_street, 1, wx.EXPAND)
2198 SZR_input.Add(STT_address_number, 0, wx.SHAPED)
2199 SZR_input.Add(self.TTC_address_number, 1, wx.EXPAND)
2200 SZR_input.Add(STT_town, 0, wx.SHAPED)
2201 SZR_input.Add(self.PRW_town, 1, wx.EXPAND)
2202 SZR_input.Add(STT_state, 0, wx.SHAPED)
2203 SZR_input.Add(self.PRW_state, 1, wx.EXPAND)
2204 SZR_input.Add(STT_country, 0, wx.SHAPED)
2205 SZR_input.Add(self.PRW_country, 1, wx.EXPAND)
2206 SZR_input.Add(STT_phone, 0, wx.SHAPED)
2207 SZR_input.Add(self.TTC_phone, 1, wx.EXPAND)
2208 SZR_input.Add(STT_occupation, 0, wx.SHAPED)
2209 SZR_input.Add(self.PRW_occupation, 1, wx.EXPAND)
2210 SZR_input.Add(STT_comment, 0, wx.SHAPED)
2211 SZR_input.Add(self.TCTRL_comment, 1, wx.EXPAND)
2212
2213 PNL_form.SetSizerAndFit(SZR_input)
2214
2215
2216 SZR_main = makePageTitle(self, self.__title)
2217 SZR_main.Add(PNL_form, 1, wx.EXPAND)
2218
2219
2220
2225
2226 - def on_country_selected(self, data):
2227 """Set the states according to entered country."""
2228 self.PRW_state.set_context(context=u'country', val=data)
2229 return True
2230
2231 - def on_name_set(self):
2232 """Set the gender according to entered firstname.
2233
2234 Matches are fetched from existing records in backend.
2235 """
2236 firstname = self.PRW_firstname.GetValue().strip()
2237 rows, idx = gmPG2.run_ro_queries(queries = [{
2238 'cmd': u"select gender from dem.name_gender_map where name ilike %s",
2239 'args': [firstname]
2240 }])
2241 if len(rows) == 0:
2242 return True
2243 wx.CallAfter(self.PRW_gender.SetData, rows[0][0])
2244 return True
2245
2246 - def on_zip_set(self):
2247 """Set the street, town, state and country according to entered zip code."""
2248 zip_code = self.PRW_zip_code.GetValue().strip()
2249 self.PRW_street.set_context(context=u'zip', val=zip_code)
2250 self.PRW_town.set_context(context=u'zip', val=zip_code)
2251 self.PRW_state.set_context(context=u'zip', val=zip_code)
2252 self.PRW_country.set_context(context=u'zip', val=zip_code)
2253 return True
2254
2255 -def makePageTitle(wizPg, title):
2256 """
2257 Utility function to create the main sizer of a wizard's page.
2258
2259 @param wizPg The wizard page widget
2260 @type wizPg A wx.WizardPageSimple instance
2261 @param title The wizard page's descriptive title
2262 @type title A StringType instance
2263 """
2264 sizer = wx.BoxSizer(wx.VERTICAL)
2265 wizPg.SetSizer(sizer)
2266 title = wx.StaticText(wizPg, -1, title)
2267 title.SetFont(wx.Font(10, wx.SWISS, wx.NORMAL, wx.BOLD))
2268 sizer.Add(title, 0, wx.ALIGN_CENTRE|wx.ALL, 2)
2269 sizer.Add(wx.StaticLine(wizPg, -1), 0, wx.EXPAND|wx.ALL, 2)
2270 return sizer
2271
2273 """
2274 Wizard to create a new patient.
2275
2276 TODO:
2277 - write pages for different "themes" of patient creation
2278 - make it configurable which pages are loaded
2279 - make available sets of pages that apply to a country
2280 - make loading of some pages depend upon values in earlier pages, eg
2281 when the patient is female and older than 13 include a page about
2282 "female" data (number of kids etc)
2283
2284 FIXME: use: wizard.FindWindowById(wx.ID_FORWARD).Disable()
2285 """
2286
2287 - def __init__(self, parent, title = _('Register new person'), subtitle = _('Basic demographic details') ):
2288 """
2289 Creates a new instance of NewPatientWizard
2290 @param parent - The parent widget
2291 @type parent - A wx.Window instance
2292 """
2293 id_wiz = wx.NewId()
2294 wx.wizard.Wizard.__init__(self, parent, id_wiz, title)
2295 self.SetExtraStyle(wx.WS_EX_VALIDATE_RECURSIVELY)
2296 self.__subtitle = subtitle
2297 self.__do_layout()
2298
2300 """Create new patient.
2301
2302 activate, too, if told to do so (and patient successfully created)
2303 """
2304 while True:
2305
2306 if not wx.wizard.Wizard.RunWizard(self, self.basic_pat_details):
2307 return False
2308
2309 try:
2310
2311 ident = create_identity_from_dtd(dtd = self.basic_pat_details.form_DTD)
2312 except:
2313 _log.exception('cannot add new patient - missing identity fields')
2314 gmGuiHelpers.gm_show_error (
2315 _('Cannot create new patient.\n'
2316 'Missing parts of the identity.'
2317 ),
2318 _('Adding new patient')
2319 )
2320 continue
2321
2322 update_identity_from_dtd(identity = ident, dtd = self.basic_pat_details.form_DTD)
2323
2324 try:
2325 link_contacts_from_dtd(identity = ident, dtd = self.basic_pat_details.form_DTD)
2326 except:
2327 _log.exception('cannot finalize new patient - missing address fields')
2328 gmGuiHelpers.gm_show_error (
2329 _('Cannot add address for the new patient.\n'
2330 'You must either enter all of the address fields or\n'
2331 'none at all. The relevant fields are marked in yellow.\n'
2332 '\n'
2333 'You will need to add the address details in the\n'
2334 'demographics module.'
2335 ),
2336 _('Adding new patient')
2337 )
2338 break
2339
2340 link_occupation_from_dtd(identity = ident, dtd = self.basic_pat_details.form_DTD)
2341
2342 break
2343
2344 if activate:
2345 from Gnumed.wxpython import gmPatSearchWidgets
2346 gmPatSearchWidgets.set_active_patient(patient = ident)
2347
2348 return ident
2349
2350
2351
2353 """Arrange widgets.
2354 """
2355
2356 self.basic_pat_details = cBasicPatDetailsPage(self, self.__subtitle )
2357 self.FitToPage(self.basic_pat_details)
2358
2359
2361 """
2362 This validator is used to ensure that the user has entered all
2363 the required conditional values in the page (eg., to properly
2364 create an address, all the related fields must be filled).
2365 """
2366
2367 - def __init__(self, dtd):
2368 """
2369 Validator initialization.
2370 @param dtd The object containing the data model.
2371 @type dtd A cFormDTD instance
2372 """
2373
2374 wx.PyValidator.__init__(self)
2375
2376 self.form_DTD = dtd
2377
2379 """
2380 Standard cloner.
2381 Note that every validator must implement the Clone() method.
2382 """
2383 return cBasicPatDetailsPageValidator(dtd = self.form_DTD)
2384
2385 - def Validate(self, parent = None):
2386 """
2387 Validate the contents of the given text control.
2388 """
2389 _pnl_form = self.GetWindow().GetParent()
2390
2391 error = False
2392
2393
2394 if _pnl_form.PRW_lastname.GetValue().strip() == '':
2395 error = True
2396 gmDispatcher.send(signal = 'statustext', msg = _('Must enter lastname.'))
2397 _pnl_form.PRW_lastname.SetBackgroundColour('pink')
2398 _pnl_form.PRW_lastname.Refresh()
2399 else:
2400 _pnl_form.PRW_lastname.SetBackgroundColour(wx.SystemSettings_GetColour(wx.SYS_COLOUR_WINDOW))
2401 _pnl_form.PRW_lastname.Refresh()
2402
2403 if _pnl_form.PRW_firstname.GetValue().strip() == '':
2404 error = True
2405 gmDispatcher.send(signal = 'statustext', msg = _('Must enter first name.'))
2406 _pnl_form.PRW_firstname.SetBackgroundColour('pink')
2407 _pnl_form.PRW_firstname.Refresh()
2408 else:
2409 _pnl_form.PRW_firstname.SetBackgroundColour(wx.SystemSettings_GetColour(wx.SYS_COLOUR_WINDOW))
2410 _pnl_form.PRW_firstname.Refresh()
2411
2412
2413 if _pnl_form.PRW_gender.GetData() is None:
2414 error = True
2415 gmDispatcher.send(signal = 'statustext', msg = _('Must select gender.'))
2416 _pnl_form.PRW_gender.SetBackgroundColour('pink')
2417 _pnl_form.PRW_gender.Refresh()
2418 else:
2419 _pnl_form.PRW_gender.SetBackgroundColour(wx.SystemSettings_GetColour(wx.SYS_COLOUR_WINDOW))
2420 _pnl_form.PRW_gender.Refresh()
2421
2422
2423 if (
2424 (_pnl_form.PRW_dob.GetValue().strip() == u'')
2425 or (not _pnl_form.PRW_dob.is_valid_timestamp())
2426 or (_pnl_form.PRW_dob.GetData().timestamp.year < 1900)
2427 ):
2428 error = True
2429 msg = _('Cannot parse <%s> into proper timestamp.') % _pnl_form.PRW_dob.GetValue()
2430 gmDispatcher.send(signal = 'statustext', msg = msg)
2431 _pnl_form.PRW_dob.SetBackgroundColour('pink')
2432 _pnl_form.PRW_dob.Refresh()
2433 else:
2434 _pnl_form.PRW_dob.SetBackgroundColour(wx.SystemSettings_GetColour(wx.SYS_COLOUR_WINDOW))
2435 _pnl_form.PRW_dob.Refresh()
2436
2437
2438 is_any_field_filled = False
2439 address_fields = (
2440 _pnl_form.TTC_address_number,
2441 _pnl_form.PRW_zip_code,
2442 _pnl_form.PRW_street,
2443 _pnl_form.PRW_town
2444 )
2445 for field in address_fields:
2446 if field.GetValue().strip() == u'':
2447 if is_any_field_filled:
2448 error = True
2449 msg = _('To properly create an address, all the related fields must be filled in.')
2450 gmGuiHelpers.gm_show_error(msg, _('Required fields'))
2451 field.SetBackgroundColour('pink')
2452 field.SetFocus()
2453 field.Refresh()
2454 else:
2455 is_any_field_filled = True
2456 field.SetBackgroundColour(wx.SystemSettings_GetColour(wx.SYS_COLOUR_WINDOW))
2457 field.Refresh()
2458
2459 address_fields = (
2460 _pnl_form.PRW_state,
2461 _pnl_form.PRW_country
2462 )
2463 for field in address_fields:
2464 if field.GetData() is None:
2465 if is_any_field_filled:
2466 error = True
2467 msg = _('To properly create an address, all the related fields must be filled in.')
2468 gmGuiHelpers.gm_show_error(msg, _('Required fields'))
2469 field.SetBackgroundColour('pink')
2470 field.SetFocus()
2471 field.Refresh()
2472 else:
2473 is_any_field_filled = True
2474 field.SetBackgroundColour(wx.SystemSettings_GetColour(wx.SYS_COLOUR_WINDOW))
2475 field.Refresh()
2476
2477 return (not error)
2478
2479 - def TransferToWindow(self):
2480 """
2481 Transfer data from validator to window.
2482 The default implementation returns False, indicating that an error
2483 occurred. We simply return True, as we don't do any data transfer.
2484 """
2485 _pnl_form = self.GetWindow().GetParent()
2486
2487 _pnl_form.PRW_gender.SetData(self.form_DTD['gender'])
2488 _pnl_form.PRW_dob.SetText(self.form_DTD['dob'])
2489 _pnl_form.PRW_lastname.SetText(self.form_DTD['lastnames'])
2490 _pnl_form.PRW_firstname.SetText(self.form_DTD['firstnames'])
2491 _pnl_form.PRW_title.SetText(self.form_DTD['title'])
2492 _pnl_form.PRW_nick.SetText(self.form_DTD['nick'])
2493 _pnl_form.PRW_occupation.SetText(self.form_DTD['occupation'])
2494 _pnl_form.TTC_address_number.SetValue(self.form_DTD['address_number'])
2495 _pnl_form.PRW_street.SetText(self.form_DTD['street'])
2496 _pnl_form.PRW_zip_code.SetText(self.form_DTD['zip_code'])
2497 _pnl_form.PRW_town.SetText(self.form_DTD['town'])
2498 _pnl_form.PRW_state.SetData(self.form_DTD['state'])
2499 _pnl_form.PRW_country.SetData(self.form_DTD['country'])
2500 _pnl_form.TTC_phone.SetValue(self.form_DTD['phone'])
2501 _pnl_form.TCTRL_comment.SetValue(self.form_DTD['comment'])
2502 return True
2503
2505 """
2506 Transfer data from window to validator.
2507 The default implementation returns False, indicating that an error
2508 occurred. We simply return True, as we don't do any data transfer.
2509 """
2510
2511 if not self.GetWindow().GetParent().Validate():
2512 return False
2513 try:
2514 _pnl_form = self.GetWindow().GetParent()
2515
2516 self.form_DTD['gender'] = _pnl_form.PRW_gender.GetData()
2517 self.form_DTD['dob'] = _pnl_form.PRW_dob.GetData()
2518
2519 self.form_DTD['lastnames'] = _pnl_form.PRW_lastname.GetValue()
2520 self.form_DTD['firstnames'] = _pnl_form.PRW_firstname.GetValue()
2521 self.form_DTD['title'] = _pnl_form.PRW_title.GetValue()
2522 self.form_DTD['nick'] = _pnl_form.PRW_nick.GetValue()
2523
2524 self.form_DTD['occupation'] = _pnl_form.PRW_occupation.GetValue()
2525
2526 self.form_DTD['address_number'] = _pnl_form.TTC_address_number.GetValue()
2527 self.form_DTD['street'] = _pnl_form.PRW_street.GetValue()
2528 self.form_DTD['zip_code'] = _pnl_form.PRW_zip_code.GetValue()
2529 self.form_DTD['town'] = _pnl_form.PRW_town.GetValue()
2530 self.form_DTD['state'] = _pnl_form.PRW_state.GetData()
2531 self.form_DTD['country'] = _pnl_form.PRW_country.GetData()
2532
2533 self.form_DTD['phone'] = _pnl_form.TTC_phone.GetValue()
2534
2535 self.form_DTD['comment'] = _pnl_form.TCTRL_comment.GetValue()
2536 except:
2537 return False
2538 return True
2539
2541 """
2542 Utility class to test the new patient wizard.
2543 """
2544
2546 """
2547 Create a new instance of TestPanel.
2548 @param parent The parent widget
2549 @type parent A wx.Window instance
2550 """
2551 wx.Panel.__init__(self, parent, id)
2552 wizard = cNewPatientWizard(self)
2553 print wizard.RunWizard()
2554
2555 if __name__ == "__main__":
2556
2557
2559 app = wx.PyWidgetTester(size = (600, 400))
2560 app.SetWidget(cKOrganizerSchedulePnl)
2561 app.MainLoop()
2562
2564 app = wx.PyWidgetTester(size = (600, 400))
2565 widget = cPersonNamesManagerPnl(app.frame, -1)
2566 widget.identity = activate_patient()
2567 app.frame.Show(True)
2568 app.MainLoop()
2569
2571 app = wx.PyWidgetTester(size = (600, 400))
2572 widget = cPersonIDsManagerPnl(app.frame, -1)
2573 widget.identity = activate_patient()
2574 app.frame.Show(True)
2575 app.MainLoop()
2576
2578 app = wx.PyWidgetTester(size = (600, 400))
2579 widget = cPersonIdentityManagerPnl(app.frame, -1)
2580 widget.identity = activate_patient()
2581 app.frame.Show(True)
2582 app.MainLoop()
2583
2588
2595
2597 app = wx.PyWidgetTester(size = (600, 400))
2598 widget = cPersonDemographicsEditorNb(app.frame, -1)
2599 widget.identity = activate_patient()
2600 widget.refresh()
2601 app.frame.Show(True)
2602 app.MainLoop()
2603
2612
2613 if len(sys.argv) > 1 and sys.argv[1] == 'test':
2614
2615 gmI18N.activate_locale()
2616 gmI18N.install_domain(domain='gnumed')
2617 gmPG2.get_connection()
2618
2619
2620
2621
2622
2623
2624
2625
2626
2627
2628
2629
2630
2631
2632
2633
2634 test_urb_prw()
2635
2636
2637
2638
2639
2640
2641
2642
2643
2644
2645
2646
2647
2648
2649
2650
2651
2652