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
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._TCTRL_number.SetValue(adr['number'])
1578 self._TCTRL_unit.SetValue(gmTools.coalesce(adr['subunit'], u''))
1579
1580 self._PRW_urb.SetText(value = adr['urb'], data = adr['urb'])
1581 self._PRW_urb.set_context(context = u'zip', val = adr['postcode'])
1582
1583 self._PRW_region.SetText(value = adr['l10n_state'], data = adr['code_state'])
1584 self._PRW_region.set_context(context = u'zip', val = adr['postcode'])
1585
1586 self._PRW_country.SetText(value = adr['l10n_country'], data = adr['code_country'])
1587 self._PRW_country.set_context(context = u'zip', val = adr['postcode'])
1588
1590 error = False
1591
1592
1593 if self._PRW_lastname.GetValue().strip() == u'':
1594 error = True
1595 gmDispatcher.send(signal = 'statustext', msg = _('Must enter lastname.'))
1596 self._PRW_lastname.display_as_valid(False)
1597 else:
1598 self._PRW_lastname.display_as_valid(True)
1599
1600 if self._PRW_firstnames.GetValue().strip() == '':
1601 error = True
1602 gmDispatcher.send(signal = 'statustext', msg = _('Must enter first name.'))
1603 self._PRW_firstnames.display_as_valid(False)
1604 else:
1605 self._PRW_firstnames.display_as_valid(True)
1606
1607
1608 if self._PRW_gender.GetData() is None:
1609 error = True
1610 gmDispatcher.send(signal = 'statustext', msg = _('Must select gender.'))
1611 self._PRW_gender.display_as_valid(False)
1612 else:
1613 self._PRW_gender.display_as_valid(True)
1614
1615
1616 if not _validate_dob_field(self._PRW_dob):
1617 error = True
1618
1619
1620
1621
1622 return (not error)
1623
1625
1626
1627 if self._PRW_address_searcher.GetData() is not None:
1628 wx.CallAfter(self.__set_fields_from_address_searcher)
1629 return True
1630
1631
1632 fields_to_fill = (
1633 self._TCTRL_number,
1634 self._PRW_zip,
1635 self._PRW_street,
1636 self._PRW_urb,
1637 self._PRW_region,
1638 self._PRW_country
1639 )
1640 no_of_filled_fields = 0
1641
1642 for field in fields_to_fill:
1643 if field.GetValue().strip() != u'':
1644 no_of_filled_fields += 1
1645 field.SetBackgroundColour(gmPhraseWheel.color_prw_valid)
1646 field.Refresh()
1647
1648
1649 if no_of_filled_fields == 0:
1650 if empty_address_is_valid:
1651 return True
1652 else:
1653 return None
1654
1655
1656 if no_of_filled_fields != len(fields_to_fill):
1657 for field in fields_to_fill:
1658 if field.GetValue().strip() == u'':
1659 field.SetBackgroundColour(gmPhraseWheel.color_prw_invalid)
1660 field.SetFocus()
1661 field.Refresh()
1662 msg = _('To properly create an address, all the related fields must be filled in.')
1663 gmGuiHelpers.gm_show_error(msg, _('Required fields'))
1664 return False
1665
1666
1667
1668
1669 strict_fields = (
1670 self._PRW_region,
1671 self._PRW_country
1672 )
1673 error = False
1674 for field in strict_fields:
1675 if field.GetData() is None:
1676 error = True
1677 field.SetBackgroundColour(gmPhraseWheel.color_prw_invalid)
1678 field.SetFocus()
1679 else:
1680 field.SetBackgroundColour(gmPhraseWheel.color_prw_valid)
1681 field.Refresh()
1682
1683 if error:
1684 msg = _('This field must contain an item selected from the dropdown list.')
1685 gmGuiHelpers.gm_show_error(msg, _('Required fields'))
1686 return False
1687
1688 return True
1689
1707
1708
1709
1711 """Set the gender according to entered firstname.
1712
1713 Matches are fetched from existing records in backend.
1714 """
1715
1716
1717 if self._PRW_gender.GetData() is not None:
1718 return True
1719
1720 firstname = self._PRW_firstnames.GetValue().strip()
1721 if firstname == u'':
1722 return True
1723
1724 gender = gmPerson.map_firstnames2gender(firstnames = firstname)
1725 if gender is None:
1726 return True
1727
1728 wx.CallAfter(self._PRW_gender.SetData, gender)
1729 return True
1730
1732 self.__perhaps_invalidate_address_searcher(self._PRW_zip, 'postcode')
1733
1734 zip_code = gmTools.none_if(self._PRW_zip.GetValue().strip(), u'')
1735 self._PRW_street.set_context(context = u'zip', val = zip_code)
1736 self._PRW_urb.set_context(context = u'zip', val = zip_code)
1737 self._PRW_region.set_context(context = u'zip', val = zip_code)
1738 self._PRW_country.set_context(context = u'zip', val = zip_code)
1739
1740 return True
1741
1743 self.__perhaps_invalidate_address_searcher(self._PRW_country, 'l10n_country')
1744
1745 country = gmTools.none_if(self._PRW_country.GetValue().strip(), u'')
1746 self._PRW_region.set_context(context = u'country', val = country)
1747
1748 return True
1749
1751 mapping = [
1752 (self._PRW_street, 'street'),
1753 (self._TCTRL_number, 'number'),
1754 (self._TCTRL_unit, 'subunit'),
1755 (self._PRW_urb, 'urb'),
1756 (self._PRW_region, 'l10n_state')
1757 ]
1758
1759 for ctrl, field in mapping:
1760 if self.__perhaps_invalidate_address_searcher(ctrl, field):
1761 return True
1762
1763 return True
1764
1766 if self._PRW_address_searcher.address is None:
1767 return True
1768
1769 wx.CallAfter(self.__set_fields_from_address_searcher)
1770 return True
1771
1772
1773
1775 if self._PRW_primary_provider.GetValue().strip() == u'':
1776 self._PRW_primary_provider.display_as_valid(True)
1777 else:
1778 if self._PRW_primary_provider.GetData() is None:
1779 self._PRW_primary_provider.display_as_valid(False)
1780 else:
1781 self._PRW_primary_provider.display_as_valid(True)
1782 return (self.__identity_valid_for_save() and self.__address_valid_for_save(empty_address_is_valid = True))
1783
1785
1786 if self._PRW_dob.GetValue().strip() == u'':
1787 if not _empty_dob_allowed():
1788 self._PRW_dob.display_as_valid(False)
1789 self._PRW_dob.SetFocus()
1790 return False
1791
1792
1793 new_identity = gmPerson.create_identity (
1794 gender = self._PRW_gender.GetData(),
1795 dob = self._PRW_dob.GetData(),
1796 lastnames = self._PRW_lastname.GetValue().strip(),
1797 firstnames = self._PRW_firstnames.GetValue().strip()
1798 )
1799 _log.debug('identity created: %s' % new_identity)
1800
1801 new_identity['title'] = gmTools.none_if(self._PRW_title.GetValue().strip())
1802 new_identity.set_nickname(nickname = gmTools.none_if(self._PRW_nickname.GetValue().strip(), u''))
1803
1804 prov = self._PRW_primary_provider.GetData()
1805 if prov is not None:
1806 new_identity['pk_primary_provider'] = prov
1807 new_identity.save()
1808
1809 name = new_identity.get_active_name()
1810 name['comment'] = gmTools.none_if(self._TCTRL_comment.GetValue().strip(), u'')
1811 name.save()
1812
1813
1814 is_valid = self.__address_valid_for_save(empty_address_is_valid = False)
1815 if is_valid is True:
1816
1817
1818 try:
1819 new_identity.link_address (
1820 number = self._TCTRL_number.GetValue().strip(),
1821 street = self._PRW_street.GetValue().strip(),
1822 postcode = self._PRW_zip.GetValue().strip(),
1823 urb = self._PRW_urb.GetValue().strip(),
1824 state = self._PRW_region.GetData(),
1825 country = self._PRW_country.GetData(),
1826 subunit = gmTools.none_if(self._TCTRL_unit.GetValue().strip(), u'')
1827 )
1828 except gmPG2.dbapi.InternalError:
1829 _log.debug('number: >>%s<<', self._TCTRL_number.GetValue().strip())
1830 _log.debug('(sub)unit: >>%s<<', self._TCTRL_unit.GetValue().strip())
1831 _log.debug('street: >>%s<<', self._PRW_street.GetValue().strip())
1832 _log.debug('postcode: >>%s<<', self._PRW_zip.GetValue().strip())
1833 _log.debug('urb: >>%s<<', self._PRW_urb.GetValue().strip())
1834 _log.debug('state: >>%s<<', self._PRW_region.GetData().strip())
1835 _log.debug('country: >>%s<<', self._PRW_country.GetData().strip())
1836 _log.exception('cannot link address')
1837 gmGuiHelpers.gm_show_error (
1838 aTitle = _('Saving address'),
1839 aMessage = _(
1840 'Cannot save this address.\n'
1841 '\n'
1842 'You will have to add it via the Demographics plugin.\n'
1843 )
1844 )
1845 elif is_valid is False:
1846 gmGuiHelpers.gm_show_error (
1847 aTitle = _('Saving address'),
1848 aMessage = _(
1849 'Address not saved.\n'
1850 '\n'
1851 'You will have to add it via the Demographics plugin.\n'
1852 )
1853 )
1854
1855
1856
1857 channel_name = self._PRW_channel_type.GetValue().strip()
1858 pk_channel_type = self._PRW_channel_type.GetData()
1859 if pk_channel_type is None:
1860 if channel_name == u'':
1861 channel_name = u'homephone'
1862 new_identity.link_comm_channel (
1863 comm_medium = channel_name,
1864 pk_channel_type = pk_channel_type,
1865 url = gmTools.none_if(self._TCTRL_phone.GetValue().strip(), u''),
1866 is_confidential = False
1867 )
1868
1869
1870 pk_type = self._PRW_external_id_type.GetData()
1871 id_value = self._TCTRL_external_id_value.GetValue().strip()
1872 if (pk_type is not None) and (id_value != u''):
1873 new_identity.add_external_id(value = id_value, pk_type = pk_type)
1874
1875
1876 new_identity.link_occupation (
1877 occupation = gmTools.none_if(self._PRW_occupation.GetValue().strip(), u'')
1878 )
1879
1880 self.data = new_identity
1881 return True
1882
1884 raise NotImplementedError('[%s]: not expected to be used' % self.__class__.__name__)
1885
1889
1892
1894 raise NotImplementedError('[%s]: not expected to be used' % self.__class__.__name__)
1895
1896
1897
1898
1900 """Notebook displaying demographics editing pages:
1901
1902 - Contacts (addresses, phone numbers, etc)
1903 - Identity
1904 - Social network (significant others, GP, etc)
1905
1906 Does NOT act on/listen to the current patient.
1907 """
1908
1910
1911 wx.Notebook.__init__ (
1912 self,
1913 parent = parent,
1914 id = id,
1915 style = wx.NB_TOP | wx.NB_MULTILINE | wx.NO_BORDER,
1916 name = self.__class__.__name__
1917 )
1918
1919 self.__identity = None
1920 self.__do_layout()
1921 self.SetSelection(0)
1922
1923
1924
1926 """Populate fields in pages with data from model."""
1927 for page_idx in range(self.GetPageCount()):
1928 page = self.GetPage(page_idx)
1929 page.identity = self.__identity
1930
1931 return True
1932
1933
1934
1964
1965
1966
1968 return self.__identity
1969
1972
1973 identity = property(_get_identity, _set_identity)
1974
1975
1976
1977
1978
1979
1981 """Page containing patient occupations edition fields.
1982 """
1983 - def __init__(self, parent, id, ident=None):
1984 """
1985 Creates a new instance of BasicPatDetailsPage
1986 @param parent - The parent widget
1987 @type parent - A wx.Window instance
1988 @param id - The widget id
1989 @type id - An integer
1990 """
1991 wx.Panel.__init__(self, parent, id)
1992 self.__ident = ident
1993 self.__do_layout()
1994
1996 PNL_form = wx.Panel(self, -1)
1997
1998 STT_occupation = wx.StaticText(PNL_form, -1, _('Occupation'))
1999 self.PRW_occupation = cOccupationPhraseWheel(parent = PNL_form, id = -1)
2000 self.PRW_occupation.SetToolTipString(_("primary occupation of the patient"))
2001
2002 STT_occupation_updated = wx.StaticText(PNL_form, -1, _('Last updated'))
2003 self.TTC_occupation_updated = wx.TextCtrl(PNL_form, -1, style = wx.TE_READONLY)
2004
2005
2006 SZR_input = wx.FlexGridSizer(cols = 2, rows = 5, vgap = 4, hgap = 4)
2007 SZR_input.AddGrowableCol(1)
2008 SZR_input.Add(STT_occupation, 0, wx.SHAPED)
2009 SZR_input.Add(self.PRW_occupation, 1, wx.EXPAND)
2010 SZR_input.Add(STT_occupation_updated, 0, wx.SHAPED)
2011 SZR_input.Add(self.TTC_occupation_updated, 1, wx.EXPAND)
2012 PNL_form.SetSizerAndFit(SZR_input)
2013
2014
2015 SZR_main = wx.BoxSizer(wx.VERTICAL)
2016 SZR_main.Add(PNL_form, 1, wx.EXPAND)
2017 self.SetSizer(SZR_main)
2018
2021
2022 - def refresh(self, identity=None):
2023 if identity is not None:
2024 self.__ident = identity
2025 jobs = self.__ident.get_occupations()
2026 if len(jobs) > 0:
2027 self.PRW_occupation.SetText(jobs[0]['l10n_occupation'])
2028 self.TTC_occupation_updated.SetValue(jobs[0]['modified_when'].strftime('%m/%Y'))
2029 return True
2030
2032 if self.PRW_occupation.IsModified():
2033 new_job = self.PRW_occupation.GetValue().strip()
2034 jobs = self.__ident.get_occupations()
2035 for job in jobs:
2036 if job['l10n_occupation'] == new_job:
2037 continue
2038 self.__ident.unlink_occupation(occupation = job['l10n_occupation'])
2039 self.__ident.link_occupation(occupation = new_job)
2040 return True
2041
2043 """Patient demographics plugin for main notebook.
2044
2045 Hosts another notebook with pages for Identity, Contacts, etc.
2046
2047 Acts on/listens to the currently active patient.
2048 """
2049
2055
2056
2057
2058
2059
2060
2062 """Arrange widgets."""
2063 self.__patient_notebook = cPersonDemographicsEditorNb(self, -1)
2064
2065 szr_main = wx.BoxSizer(wx.VERTICAL)
2066 szr_main.Add(self.__patient_notebook, 1, wx.EXPAND)
2067 self.SetSizerAndFit(szr_main)
2068
2069
2070
2072 gmDispatcher.connect(signal = u'pre_patient_selection', receiver = self._on_pre_patient_selection)
2073 gmDispatcher.connect(signal = u'post_patient_selection', receiver = self._on_post_patient_selection)
2074
2076 self._schedule_data_reget()
2077
2079 self._schedule_data_reget()
2080 print "_on_post_patient_selection: scheduled"
2081
2082
2092
2093
2094 if __name__ == "__main__":
2095
2096
2098 app = wx.PyWidgetTester(size = (600, 400))
2099 app.SetWidget(cKOrganizerSchedulePnl)
2100 app.MainLoop()
2101
2103 app = wx.PyWidgetTester(size = (600, 400))
2104 widget = cPersonNamesManagerPnl(app.frame, -1)
2105 widget.identity = activate_patient()
2106 app.frame.Show(True)
2107 app.MainLoop()
2108
2110 app = wx.PyWidgetTester(size = (600, 400))
2111 widget = cPersonIDsManagerPnl(app.frame, -1)
2112 widget.identity = activate_patient()
2113 app.frame.Show(True)
2114 app.MainLoop()
2115
2117 app = wx.PyWidgetTester(size = (600, 400))
2118 widget = cPersonIdentityManagerPnl(app.frame, -1)
2119 widget.identity = activate_patient()
2120 app.frame.Show(True)
2121 app.MainLoop()
2122
2127
2129 app = wx.PyWidgetTester(size = (600, 400))
2130 widget = cPersonDemographicsEditorNb(app.frame, -1)
2131 widget.identity = activate_patient()
2132 widget.refresh()
2133 app.frame.Show(True)
2134 app.MainLoop()
2135
2144
2145 if len(sys.argv) > 1 and sys.argv[1] == 'test':
2146
2147 gmI18N.activate_locale()
2148 gmI18N.install_domain(domain='gnumed')
2149 gmPG2.get_connection()
2150
2151
2152
2153
2154
2155
2156
2157
2158
2159
2160
2161 test_person_ids_pnl()
2162
2163
2164
2165
2166
2167
2168