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 ON (list_label)
644 pk AS data,
645 name AS field_label,
646 name || coalesce(' (' || issuer || ')', '') as list_label
647 FROM dem.enum_ext_id_types
648 WHERE name %(fragment_condition)s
649 ORDER BY list_label
650 LIMIT 25
651 """
652 mp = gmMatchProvider.cMatchProvider_SQL2(queries=query)
653 mp.setThresholds(1, 3, 5)
654 gmPhraseWheel.cPhraseWheel.__init__(self, *args, **kwargs)
655 self.SetToolTipString(_("Enter or select a type for the external ID."))
656 self.matcher = mp
657
662
677
678
679
680 from Gnumed.wxGladeWidgets import wxgExternalIDEditAreaPnl
681
682 -class cExternalIDEditAreaPnl(wxgExternalIDEditAreaPnl.wxgExternalIDEditAreaPnl, gmEditArea.cGenericEditAreaMixin):
683 """An edit area for editing/creating external IDs.
684
685 Does NOT act on/listen to the current patient.
686 """
706
709
710
711
732
750
766
772
774 self._refresh_as_new()
775 self._PRW_issuer.SetText(self.data['issuer'])
776
782
783
784
786 """Set the issuer according to the selected type.
787
788 Matches are fetched from existing records in backend.
789 """
790 pk_curr_type = self._PRW_type.GetData()
791 if pk_curr_type is None:
792 return True
793 rows, idx = gmPG2.run_ro_queries(queries = [{
794 'cmd': u"SELECT issuer from dem.enum_ext_id_types where pk = %s",
795 'args': [pk_curr_type]
796 }])
797 if len(rows) == 0:
798 return True
799 wx.CallAfter(self._PRW_issuer.SetText, rows[0][0])
800 return True
801
802
803
804
806 allow_empty_dob = gmGuiHelpers.gm_show_question (
807 _(
808 'Are you sure you want to leave this person\n'
809 'without a valid date of birth ?\n'
810 '\n'
811 'This can be useful for temporary staff members\n'
812 'but will provoke nag screens if this person\n'
813 'becomes a patient.\n'
814 ),
815 _('Validating date of birth')
816 )
817 return allow_empty_dob
818
820
821
822 if dob_prw.is_valid_timestamp(allow_empty = False):
823 dob = dob_prw.date
824
825 if (dob.year > 1899) and (dob < gmDateTime.pydt_now_here()):
826 return True
827
828 if dob.year < 1900:
829 msg = _(
830 'DOB: %s\n'
831 '\n'
832 'While this is a valid point in time Python does\n'
833 'not know how to deal with it.\n'
834 '\n'
835 'We suggest using January 1st 1901 instead and adding\n'
836 'the true date of birth to the patient comment.\n'
837 '\n'
838 'Sorry for the inconvenience %s'
839 ) % (dob, gmTools.u_frowning_face)
840 else:
841 msg = _(
842 'DOB: %s\n'
843 '\n'
844 'Date of birth in the future !'
845 ) % dob
846 gmGuiHelpers.gm_show_error (
847 msg,
848 _('Validating date of birth')
849 )
850 dob_prw.display_as_valid(False)
851 dob_prw.SetFocus()
852 return False
853
854
855 if dob_prw.GetValue().strip() != u'':
856 dob_prw.display_as_valid(False)
857 gmDispatcher.send(signal = u'statustext', msg = _('Invalid date of birth.'))
858 dob_prw.SetFocus()
859 return False
860
861
862 dob_prw.display_as_valid(False)
863 return True
864
865 from Gnumed.wxGladeWidgets import wxgIdentityEAPnl
866
867 -class cIdentityEAPnl(wxgIdentityEAPnl.wxgIdentityEAPnl, gmEditArea.cGenericEditAreaMixin):
868 """An edit area for editing/creating title/gender/dob/dod etc."""
869
885
886
887
888
889
890
891
892
894
895 has_error = False
896
897 if self._PRW_gender.GetData() is None:
898 self._PRW_gender.SetFocus()
899 has_error = True
900
901 if self.data is not None:
902 if not _validate_dob_field(self._PRW_dob):
903 has_error = True
904
905 if not self._PRW_dod.is_valid_timestamp(allow_empty = True):
906 gmDispatcher.send(signal = u'statustext', msg = _('Invalid date of death.'))
907 self._PRW_dod.SetFocus()
908 has_error = True
909
910 return (has_error is False)
911
915
932
935
964
967
968 from Gnumed.wxGladeWidgets import wxgPersonNameEAPnl
969
970 -class cPersonNameEAPnl(wxgPersonNameEAPnl.wxgPersonNameEAPnl, gmEditArea.cGenericEditAreaMixin):
971 """An edit area for editing/creating names of people.
972
973 Does NOT act on/listen to the current patient.
974 """
995
996
997
998
999
1000
1001
1002
1004 validity = True
1005
1006 if self._PRW_lastname.GetValue().strip() == u'':
1007 validity = False
1008 self._PRW_lastname.display_as_valid(False)
1009 self._PRW_lastname.SetFocus()
1010 else:
1011 self._PRW_lastname.display_as_valid(True)
1012
1013 if self._PRW_firstname.GetValue().strip() == u'':
1014 validity = False
1015 self._PRW_firstname.display_as_valid(False)
1016 self._PRW_firstname.SetFocus()
1017 else:
1018 self._PRW_firstname.display_as_valid(True)
1019
1020 return validity
1021
1023
1024 first = self._PRW_firstname.GetValue().strip()
1025 last = self._PRW_lastname.GetValue().strip()
1026 active = self._CHBOX_active.GetValue()
1027
1028 data = self.__identity.add_name(first, last, active)
1029
1030 old_nick = self.__identity['active_name']['preferred']
1031 new_nick = gmTools.none_if(self._PRW_nick.GetValue().strip(), u'')
1032 if active:
1033 data['preferred'] = gmTools.coalesce(new_nick, old_nick)
1034 else:
1035 data['preferred'] = new_nick
1036 data['comment'] = gmTools.none_if(self._TCTRL_comment.GetValue().strip(), u'')
1037 data.save()
1038
1039 self.data = data
1040 return True
1041
1043 """The knack here is that we can only update a few fields.
1044
1045 Otherwise we need to clone the name and update that.
1046 """
1047 first = self._PRW_firstname.GetValue().strip()
1048 last = self._PRW_lastname.GetValue().strip()
1049 active = self._CHBOX_active.GetValue()
1050
1051 current_name = self.data['firstnames'].strip() + self.data['lastnames'].strip()
1052 new_name = first + last
1053
1054
1055 if new_name == current_name:
1056 self.data['active_name'] = self._CHBOX_active.GetValue()
1057 self.data['preferred'] = gmTools.none_if(self._PRW_nick.GetValue().strip(), u'')
1058 self.data['comment'] = gmTools.none_if(self._TCTRL_comment.GetValue().strip(), u'')
1059 self.data.save()
1060
1061 else:
1062 name = self.__identity.add_name(first, last, active)
1063 name['preferred'] = gmTools.none_if(self._PRW_nick.GetValue().strip(), u'')
1064 name['comment'] = gmTools.none_if(self._TCTRL_comment.GetValue().strip(), u'')
1065 name.save()
1066 self.data = name
1067
1068 return True
1069
1078
1085
1094
1095
1096
1098 """A list for managing a person's names.
1099
1100 Does NOT act on/listen to the current patient.
1101 """
1119
1120
1121
1122 - def refresh(self, *args, **kwargs):
1139
1140
1141
1143 self._LCTRL_items.set_columns(columns = [
1144 _('Active'),
1145 _('Lastname'),
1146 _('Firstname(s)'),
1147 _('Preferred Name'),
1148 _('Comment')
1149 ])
1150 self._BTN_edit.SetLabel(_('Clone and &edit'))
1151
1162
1172
1174
1175 if len(self.__identity.get_names()) == 1:
1176 gmDispatcher.send(signal = u'statustext', msg = _('Cannot delete the only name of a person.'), beep = True)
1177 return False
1178
1179 go_ahead = gmGuiHelpers.gm_show_question (
1180 _( 'It is often advisable to keep old names around and\n'
1181 'just create a new "currently active" name.\n'
1182 '\n'
1183 'This allows finding the patient by both the old\n'
1184 'and the new name (think before/after marriage).\n'
1185 '\n'
1186 'Do you still want to really delete\n'
1187 "this name from the patient ?"
1188 ),
1189 _('Deleting name')
1190 )
1191 if not go_ahead:
1192 return False
1193
1194 self.__identity.delete_name(name = name)
1195 return True
1196
1197
1198
1200 return self.__identity
1201
1205
1206 identity = property(_get_identity, _set_identity)
1207
1209 """A list for managing a person's external IDs.
1210
1211 Does NOT act on/listen to the current patient.
1212 """
1230
1231
1232
1233 - def refresh(self, *args, **kwargs):
1250
1251
1252
1254 self._LCTRL_items.set_columns(columns = [
1255 _('ID type'),
1256 _('Value'),
1257 _('Issuer'),
1258 _('Comment')
1259 ])
1260
1271
1282
1284 go_ahead = gmGuiHelpers.gm_show_question (
1285 _( 'Do you really want to delete this\n'
1286 'external ID from the patient ?'),
1287 _('Deleting external ID')
1288 )
1289 if not go_ahead:
1290 return False
1291 self.__identity.delete_external_id(pk_ext_id = ext_id['pk_id'])
1292 return True
1293
1294
1295
1297 return self.__identity
1298
1302
1303 identity = property(_get_identity, _set_identity)
1304
1305
1306
1307 from Gnumed.wxGladeWidgets import wxgPersonIdentityManagerPnl
1308
1310 """A panel for editing identity data for a person.
1311
1312 - provides access to:
1313 - identity EA
1314 - name list manager
1315 - external IDs list manager
1316
1317 Does NOT act on/listen to the current patient.
1318 """
1325
1326
1327
1329 self._PNL_names.identity = self.__identity
1330 self._PNL_ids.identity = self.__identity
1331
1332 self._PNL_identity.mode = 'new'
1333 self._PNL_identity.data = self.__identity
1334 if self.__identity is not None:
1335 self._PNL_identity.mode = 'edit'
1336 self._PNL_identity._refresh_from_existing()
1337
1338
1339
1341 return self.__identity
1342
1346
1347 identity = property(_get_identity, _set_identity)
1348
1349
1350
1354
1355
1358
1359
1360 from Gnumed.wxGladeWidgets import wxgPersonSocialNetworkManagerPnl
1361
1370
1371
1372
1374
1375 tt = _('Link another person in this database as the emergency contact:\n\nEnter person name part or identifier and hit <enter>.')
1376
1377 if self.__identity is None:
1378 self._TCTRL_er_contact.SetValue(u'')
1379 self._TCTRL_person.person = None
1380 self._TCTRL_person.SetToolTipString(tt)
1381
1382 self._PRW_provider.SetText(value = u'', data = None)
1383 return
1384
1385 self._TCTRL_er_contact.SetValue(gmTools.coalesce(self.__identity['emergency_contact'], u''))
1386 if self.__identity['pk_emergency_contact'] is not None:
1387 ident = gmPerson.cIdentity(aPK_obj = self.__identity['pk_emergency_contact'])
1388 self._TCTRL_person.person = ident
1389 tt = u'%s\n\n%s\n\n%s' % (
1390 tt,
1391 ident['description_gender'],
1392 u'\n'.join([
1393 u'%s: %s%s' % (
1394 c['l10n_comm_type'],
1395 c['url'],
1396 gmTools.bool2subst(c['is_confidential'], _(' (confidential !)'), u'', u'')
1397 )
1398 for c in ident.get_comm_channels()
1399 ])
1400 )
1401 else:
1402 self._TCTRL_person.person = None
1403
1404 self._TCTRL_person.SetToolTipString(tt)
1405
1406 if self.__identity['pk_primary_provider'] is None:
1407 self._PRW_provider.SetText(value = u'', data = None)
1408 else:
1409 self._PRW_provider.SetData(data = self.__identity['pk_primary_provider'])
1410
1411
1412
1414 return self.__identity
1415
1419
1420 identity = property(_get_identity, _set_identity)
1421
1422
1423
1438
1441
1452
1460
1461
1462
1464
1465 dbcfg = gmCfg.cCfgSQL()
1466
1467 def_region = dbcfg.get2 (
1468 option = u'person.create.default_region',
1469 workplace = gmSurgery.gmCurrentPractice().active_workplace,
1470 bias = u'user'
1471 )
1472 def_country = None
1473
1474 if def_region is None:
1475 def_country = dbcfg.get2 (
1476 option = u'person.create.default_country',
1477 workplace = gmSurgery.gmCurrentPractice().active_workplace,
1478 bias = u'user'
1479 )
1480 else:
1481 countries = gmDemographicRecord.get_country_for_region(region = def_region)
1482 if len(countries) == 1:
1483 def_country = countries[0]['code_country']
1484
1485 if parent is None:
1486 parent = wx.GetApp().GetTopWindow()
1487
1488 ea = cNewPatientEAPnl(parent = parent, id = -1, country = def_country, region = def_region)
1489 dlg = gmEditArea.cGenericEditAreaDlg2(parent = parent, id = -1, edit_area = ea, single_entry = True)
1490 dlg.SetTitle(_('Adding new person'))
1491 ea._PRW_lastname.SetFocus()
1492 result = dlg.ShowModal()
1493 pat = ea.data
1494 dlg.Destroy()
1495
1496 if result != wx.ID_OK:
1497 return False
1498
1499 _log.debug('created new person [%s]', pat.ID)
1500
1501 if activate:
1502 from Gnumed.wxpython import gmPatSearchWidgets
1503 gmPatSearchWidgets.set_active_patient(patient = pat)
1504
1505 gmDispatcher.send(signal = 'display_widget', name = 'gmNotebookedPatientEditionPlugin')
1506
1507 return True
1508
1509 from Gnumed.wxGladeWidgets import wxgNewPatientEAPnl
1510
1511 -class cNewPatientEAPnl(wxgNewPatientEAPnl.wxgNewPatientEAPnl, gmEditArea.cGenericEditAreaMixin):
1512
1514
1515 try:
1516 self.default_region = kwargs['region']
1517 del kwargs['region']
1518 except KeyError:
1519 self.default_region = None
1520
1521 try:
1522 self.default_country = kwargs['country']
1523 del kwargs['country']
1524 except KeyError:
1525 self.default_country = None
1526
1527 wxgNewPatientEAPnl.wxgNewPatientEAPnl.__init__(self, *args, **kwargs)
1528 gmEditArea.cGenericEditAreaMixin.__init__(self)
1529
1530 self.mode = 'new'
1531 self.data = None
1532 self._address = None
1533
1534 self.__init_ui()
1535 self.__register_interests()
1536
1537
1538
1540 self._PRW_lastname.final_regex = '.+'
1541 self._PRW_firstnames.final_regex = '.+'
1542 self._PRW_address_searcher.selection_only = False
1543
1544
1545
1546
1547 if self.default_country is not None:
1548 match = self._PRW_country._data2match(data = self.default_country)
1549 if match is not None:
1550 self._PRW_country.SetText(value = match['field_label'], data = match['data'])
1551
1552 if self.default_region is not None:
1553 self._PRW_region.SetText(value = self.default_region)
1554
1556
1557 adr = self._PRW_address_searcher.address
1558 if adr is None:
1559 return True
1560
1561 if ctrl.GetValue().strip() != adr[field]:
1562 wx.CallAfter(self._PRW_address_searcher.SetText, value = u'', data = None)
1563 return True
1564
1565 return False
1566
1568 adr = self._PRW_address_searcher.address
1569 if adr is None:
1570 return True
1571
1572 self._PRW_zip.SetText(value = adr['postcode'], data = adr['postcode'])
1573
1574 self._PRW_street.SetText(value = adr['street'], data = adr['street'])
1575 self._PRW_street.set_context(context = u'zip', val = adr['postcode'])
1576
1577 self._PRW_urb.SetText(value = adr['urb'], data = adr['urb'])
1578 self._PRW_urb.set_context(context = u'zip', val = adr['postcode'])
1579
1580 self._PRW_region.SetText(value = adr['l10n_state'], data = adr['code_state'])
1581 self._PRW_region.set_context(context = u'zip', val = adr['postcode'])
1582
1583 self._PRW_country.SetText(value = adr['l10n_country'], data = adr['code_country'])
1584 self._PRW_country.set_context(context = u'zip', val = adr['postcode'])
1585
1587 error = False
1588
1589
1590 if self._PRW_lastname.GetValue().strip() == u'':
1591 error = True
1592 gmDispatcher.send(signal = 'statustext', msg = _('Must enter lastname.'))
1593 self._PRW_lastname.display_as_valid(False)
1594 else:
1595 self._PRW_lastname.display_as_valid(True)
1596
1597 if self._PRW_firstnames.GetValue().strip() == '':
1598 error = True
1599 gmDispatcher.send(signal = 'statustext', msg = _('Must enter first name.'))
1600 self._PRW_firstnames.display_as_valid(False)
1601 else:
1602 self._PRW_firstnames.display_as_valid(True)
1603
1604
1605 if self._PRW_gender.GetData() is None:
1606 error = True
1607 gmDispatcher.send(signal = 'statustext', msg = _('Must select gender.'))
1608 self._PRW_gender.display_as_valid(False)
1609 else:
1610 self._PRW_gender.display_as_valid(True)
1611
1612
1613 if not _validate_dob_field(self._PRW_dob):
1614 error = True
1615
1616
1617
1618
1619 return (not error)
1620
1622
1623
1624 if self._PRW_address_searcher.GetData() is not None:
1625 wx.CallAfter(self.__set_fields_from_address_searcher)
1626 return True
1627
1628
1629 fields_to_fill = (
1630 self._TCTRL_number,
1631 self._PRW_zip,
1632 self._PRW_street,
1633 self._PRW_urb,
1634 self._PRW_type
1635 )
1636 no_of_filled_fields = 0
1637
1638 for field in fields_to_fill:
1639 if field.GetValue().strip() != u'':
1640 no_of_filled_fields += 1
1641 field.SetBackgroundColour(gmPhraseWheel.color_prw_valid)
1642 field.Refresh()
1643
1644
1645 if no_of_filled_fields == 0:
1646 if empty_address_is_valid:
1647 return True
1648 else:
1649 return None
1650
1651
1652 if no_of_filled_fields != len(fields_to_fill):
1653 for field in fields_to_fill:
1654 if field.GetValue().strip() == u'':
1655 field.SetBackgroundColour(gmPhraseWheel.color_prw_invalid)
1656 field.SetFocus()
1657 field.Refresh()
1658 msg = _('To properly create an address, all the related fields must be filled in.')
1659 gmGuiHelpers.gm_show_error(msg, _('Required fields'))
1660 return False
1661
1662
1663
1664
1665 strict_fields = (
1666 self._PRW_type,
1667 self._PRW_region,
1668 self._PRW_country
1669 )
1670 error = False
1671 for field in strict_fields:
1672 if field.GetData() is None:
1673 error = True
1674 field.SetBackgroundColour(gmPhraseWheel.color_prw_invalid)
1675 field.SetFocus()
1676 else:
1677 field.SetBackgroundColour(gmPhraseWheel.color_prw_valid)
1678 field.Refresh()
1679
1680 if error:
1681 msg = _('This field must contain an item selected from the dropdown list.')
1682 gmGuiHelpers.gm_show_error(msg, _('Required fields'))
1683 return False
1684
1685 return True
1686
1704
1705
1706
1708 """Set the gender according to entered firstname.
1709
1710 Matches are fetched from existing records in backend.
1711 """
1712
1713
1714 if self._PRW_gender.GetData() is not None:
1715 return True
1716
1717 firstname = self._PRW_firstnames.GetValue().strip()
1718 if firstname == u'':
1719 return True
1720
1721 gender = gmPerson.map_firstnames2gender(firstnames = firstname)
1722 if gender is None:
1723 return True
1724
1725 wx.CallAfter(self._PRW_gender.SetData, gender)
1726 return True
1727
1729 self.__perhaps_invalidate_address_searcher(self._PRW_zip, 'postcode')
1730
1731 zip_code = gmTools.none_if(self._PRW_zip.GetValue().strip(), u'')
1732 self._PRW_street.set_context(context = u'zip', val = zip_code)
1733 self._PRW_urb.set_context(context = u'zip', val = zip_code)
1734 self._PRW_region.set_context(context = u'zip', val = zip_code)
1735 self._PRW_country.set_context(context = u'zip', val = zip_code)
1736
1737 return True
1738
1740 self.__perhaps_invalidate_address_searcher(self._PRW_country, 'l10n_country')
1741
1742 country = gmTools.none_if(self._PRW_country.GetValue().strip(), u'')
1743 self._PRW_region.set_context(context = u'country', val = country)
1744
1745 return True
1746
1748 if self._TCTRL_number.GetValue().strip() == u'':
1749 adr = self._PRW_address_searcher.address
1750 if adr is None:
1751 return True
1752 self._TCTRL_number.SetValue(adr['number'])
1753 return True
1754
1755 self.__perhaps_invalidate_address_searcher(self._TCTRL_number, 'number')
1756 return True
1757
1759 if self._TCTRL_unit.GetValue().strip() == u'':
1760 adr = self._PRW_address_searcher.address
1761 if adr is None:
1762 return True
1763 self._TCTRL_unit.SetValue(gmTools.coalesce(adr['subunit'], u''))
1764 return True
1765
1766 self.__perhaps_invalidate_address_searcher(self._TCTRL_numbunit, 'subunit')
1767 return True
1768
1770 mapping = [
1771 (self._PRW_street, 'street'),
1772 (self._PRW_urb, 'urb'),
1773 (self._PRW_region, 'l10n_state')
1774 ]
1775
1776 for ctrl, field in mapping:
1777 if self.__perhaps_invalidate_address_searcher(ctrl, field):
1778 return True
1779
1780 return True
1781
1783 if self._PRW_address_searcher.address is None:
1784 return True
1785
1786 wx.CallAfter(self.__set_fields_from_address_searcher)
1787 return True
1788
1789
1790
1792 if self._PRW_primary_provider.GetValue().strip() == u'':
1793 self._PRW_primary_provider.display_as_valid(True)
1794 else:
1795 if self._PRW_primary_provider.GetData() is None:
1796 self._PRW_primary_provider.display_as_valid(False)
1797 else:
1798 self._PRW_primary_provider.display_as_valid(True)
1799 return (self.__identity_valid_for_save() and self.__address_valid_for_save(empty_address_is_valid = True))
1800
1802
1803 if self._PRW_dob.GetValue().strip() == u'':
1804 if not _empty_dob_allowed():
1805 self._PRW_dob.display_as_valid(False)
1806 self._PRW_dob.SetFocus()
1807 return False
1808
1809
1810 new_identity = gmPerson.create_identity (
1811 gender = self._PRW_gender.GetData(),
1812 dob = self._PRW_dob.GetData(),
1813 lastnames = self._PRW_lastname.GetValue().strip(),
1814 firstnames = self._PRW_firstnames.GetValue().strip()
1815 )
1816 _log.debug('identity created: %s' % new_identity)
1817
1818 new_identity['title'] = gmTools.none_if(self._PRW_title.GetValue().strip())
1819 new_identity.set_nickname(nickname = gmTools.none_if(self._PRW_nickname.GetValue().strip(), u''))
1820
1821 prov = self._PRW_primary_provider.GetData()
1822 if prov is not None:
1823 new_identity['pk_primary_provider'] = prov
1824 new_identity['comment'] = gmTools.none_if(self._TCTRL_comment.GetValue().strip(), u'')
1825 new_identity.save()
1826
1827
1828
1829 is_valid = self.__address_valid_for_save(empty_address_is_valid = False)
1830 if is_valid is True:
1831
1832
1833 try:
1834 new_identity.link_address (
1835 number = self._TCTRL_number.GetValue().strip(),
1836 street = self._PRW_street.GetValue().strip(),
1837 postcode = self._PRW_zip.GetValue().strip(),
1838 urb = self._PRW_urb.GetValue().strip(),
1839 state = self._PRW_region.GetData(),
1840 country = self._PRW_country.GetData(),
1841 subunit = gmTools.none_if(self._TCTRL_unit.GetValue().strip(), u''),
1842 id_type = self._PRW_type.GetData()
1843 )
1844 except gmPG2.dbapi.InternalError:
1845 _log.debug('number: >>%s<<', self._TCTRL_number.GetValue().strip())
1846 _log.debug('(sub)unit: >>%s<<', self._TCTRL_unit.GetValue().strip())
1847 _log.debug('street: >>%s<<', self._PRW_street.GetValue().strip())
1848 _log.debug('postcode: >>%s<<', self._PRW_zip.GetValue().strip())
1849 _log.debug('urb: >>%s<<', self._PRW_urb.GetValue().strip())
1850 _log.debug('state: >>%s<<', self._PRW_region.GetData().strip())
1851 _log.debug('country: >>%s<<', self._PRW_country.GetData().strip())
1852 _log.exception('cannot link address')
1853 gmGuiHelpers.gm_show_error (
1854 aTitle = _('Saving address'),
1855 aMessage = _(
1856 'Cannot save this address.\n'
1857 '\n'
1858 'You will have to add it via the Demographics plugin.\n'
1859 )
1860 )
1861 elif is_valid is False:
1862 gmGuiHelpers.gm_show_error (
1863 aTitle = _('Saving address'),
1864 aMessage = _(
1865 'Address not saved.\n'
1866 '\n'
1867 'You will have to add it via the Demographics plugin.\n'
1868 )
1869 )
1870
1871
1872
1873 channel_name = self._PRW_channel_type.GetValue().strip()
1874 pk_channel_type = self._PRW_channel_type.GetData()
1875 if pk_channel_type is None:
1876 if channel_name == u'':
1877 channel_name = u'homephone'
1878 new_identity.link_comm_channel (
1879 comm_medium = channel_name,
1880 pk_channel_type = pk_channel_type,
1881 url = gmTools.none_if(self._TCTRL_phone.GetValue().strip(), u''),
1882 is_confidential = False
1883 )
1884
1885
1886 pk_type = self._PRW_external_id_type.GetData()
1887 id_value = self._TCTRL_external_id_value.GetValue().strip()
1888 if (pk_type is not None) and (id_value != u''):
1889 new_identity.add_external_id(value = id_value, pk_type = pk_type)
1890
1891
1892 new_identity.link_occupation (
1893 occupation = gmTools.none_if(self._PRW_occupation.GetValue().strip(), u'')
1894 )
1895
1896 self.data = new_identity
1897 return True
1898
1900 raise NotImplementedError('[%s]: not expected to be used' % self.__class__.__name__)
1901
1905
1908
1910 raise NotImplementedError('[%s]: not expected to be used' % self.__class__.__name__)
1911
1912
1913
1914
1916 """Notebook displaying demographics editing pages:
1917
1918 - Contacts (addresses, phone numbers, etc)
1919 - Identity
1920 - Social network (significant others, GP, etc)
1921
1922 Does NOT act on/listen to the current patient.
1923 """
1924
1926
1927 wx.Notebook.__init__ (
1928 self,
1929 parent = parent,
1930 id = id,
1931 style = wx.NB_TOP | wx.NB_MULTILINE | wx.NO_BORDER,
1932 name = self.__class__.__name__
1933 )
1934
1935 self.__identity = None
1936 self.__do_layout()
1937 self.SetSelection(0)
1938
1939
1940
1942 """Populate fields in pages with data from model."""
1943 for page_idx in range(self.GetPageCount()):
1944 page = self.GetPage(page_idx)
1945 page.identity = self.__identity
1946
1947 return True
1948
1949
1950
1980
1981
1982
1984 return self.__identity
1985
1988
1989 identity = property(_get_identity, _set_identity)
1990
1991
1992
1993
1994
1995
1997 """Page containing patient occupations edition fields.
1998 """
1999 - def __init__(self, parent, id, ident=None):
2000 """
2001 Creates a new instance of BasicPatDetailsPage
2002 @param parent - The parent widget
2003 @type parent - A wx.Window instance
2004 @param id - The widget id
2005 @type id - An integer
2006 """
2007 wx.Panel.__init__(self, parent, id)
2008 self.__ident = ident
2009 self.__do_layout()
2010
2012 PNL_form = wx.Panel(self, -1)
2013
2014 STT_occupation = wx.StaticText(PNL_form, -1, _('Occupation'))
2015 self.PRW_occupation = cOccupationPhraseWheel(parent = PNL_form, id = -1)
2016 self.PRW_occupation.SetToolTipString(_("primary occupation of the patient"))
2017
2018 STT_occupation_updated = wx.StaticText(PNL_form, -1, _('Last updated'))
2019 self.TTC_occupation_updated = wx.TextCtrl(PNL_form, -1, style = wx.TE_READONLY)
2020
2021
2022 SZR_input = wx.FlexGridSizer(cols = 2, rows = 5, vgap = 4, hgap = 4)
2023 SZR_input.AddGrowableCol(1)
2024 SZR_input.Add(STT_occupation, 0, wx.SHAPED)
2025 SZR_input.Add(self.PRW_occupation, 1, wx.EXPAND)
2026 SZR_input.Add(STT_occupation_updated, 0, wx.SHAPED)
2027 SZR_input.Add(self.TTC_occupation_updated, 1, wx.EXPAND)
2028 PNL_form.SetSizerAndFit(SZR_input)
2029
2030
2031 SZR_main = wx.BoxSizer(wx.VERTICAL)
2032 SZR_main.Add(PNL_form, 1, wx.EXPAND)
2033 self.SetSizer(SZR_main)
2034
2037
2038 - def refresh(self, identity=None):
2039 if identity is not None:
2040 self.__ident = identity
2041 jobs = self.__ident.get_occupations()
2042 if len(jobs) > 0:
2043 self.PRW_occupation.SetText(jobs[0]['l10n_occupation'])
2044 self.TTC_occupation_updated.SetValue(jobs[0]['modified_when'].strftime('%m/%Y'))
2045 return True
2046
2048 if self.PRW_occupation.IsModified():
2049 new_job = self.PRW_occupation.GetValue().strip()
2050 jobs = self.__ident.get_occupations()
2051 for job in jobs:
2052 if job['l10n_occupation'] == new_job:
2053 continue
2054 self.__ident.unlink_occupation(occupation = job['l10n_occupation'])
2055 self.__ident.link_occupation(occupation = new_job)
2056 return True
2057
2059 """Patient demographics plugin for main notebook.
2060
2061 Hosts another notebook with pages for Identity, Contacts, etc.
2062
2063 Acts on/listens to the currently active patient.
2064 """
2065
2071
2072
2073
2074
2075
2076
2078 """Arrange widgets."""
2079 self.__patient_notebook = cPersonDemographicsEditorNb(self, -1)
2080
2081 szr_main = wx.BoxSizer(wx.VERTICAL)
2082 szr_main.Add(self.__patient_notebook, 1, wx.EXPAND)
2083 self.SetSizerAndFit(szr_main)
2084
2085
2086
2088 gmDispatcher.connect(signal = u'pre_patient_selection', receiver = self._on_pre_patient_selection)
2089 gmDispatcher.connect(signal = u'post_patient_selection', receiver = self._on_post_patient_selection)
2090
2092 self._schedule_data_reget()
2093
2095 self._schedule_data_reget()
2096 print "_on_post_patient_selection: scheduled"
2097
2098
2108
2109
2110 if __name__ == "__main__":
2111
2112
2114 app = wx.PyWidgetTester(size = (600, 400))
2115 app.SetWidget(cKOrganizerSchedulePnl)
2116 app.MainLoop()
2117
2119 app = wx.PyWidgetTester(size = (600, 400))
2120 widget = cPersonNamesManagerPnl(app.frame, -1)
2121 widget.identity = activate_patient()
2122 app.frame.Show(True)
2123 app.MainLoop()
2124
2126 app = wx.PyWidgetTester(size = (600, 400))
2127 widget = cPersonIDsManagerPnl(app.frame, -1)
2128 widget.identity = activate_patient()
2129 app.frame.Show(True)
2130 app.MainLoop()
2131
2133 app = wx.PyWidgetTester(size = (600, 400))
2134 widget = cPersonIdentityManagerPnl(app.frame, -1)
2135 widget.identity = activate_patient()
2136 app.frame.Show(True)
2137 app.MainLoop()
2138
2143
2145 app = wx.PyWidgetTester(size = (600, 400))
2146 widget = cPersonDemographicsEditorNb(app.frame, -1)
2147 widget.identity = activate_patient()
2148 widget.refresh()
2149 app.frame.Show(True)
2150 app.MainLoop()
2151
2160
2161 if len(sys.argv) > 1 and sys.argv[1] == 'test':
2162
2163 gmI18N.activate_locale()
2164 gmI18N.install_domain(domain='gnumed')
2165 gmPG2.get_connection()
2166
2167
2168
2169
2170
2171
2172
2173
2174
2175
2176
2177 test_person_ids_pnl()
2178
2179
2180
2181
2182
2183
2184