| Home | Trees | Indices | Help |
|
|---|
|
|
1 """Widgets dealing with patient demographics."""
2 #============================================================
3 __author__ = "R.Terry, SJ Tan, I Haywood, Carlos Moro <cfmoro1976@yahoo.es>"
4 __license__ = 'GPL v2 or later (details at http://www.gnu.org)'
5
6 # standard library
7 import sys
8 import sys
9 import codecs
10 import re as regex
11 import logging
12 import os
13
14
15 import wx
16 import wx.wizard
17 import wx.lib.imagebrowser as wx_imagebrowser
18 import wx.lib.statbmp as wx_genstatbmp
19
20
21 # GNUmed specific
22 if __name__ == '__main__':
23 sys.path.insert(0, '../../')
24 from Gnumed.pycommon import gmDispatcher
25 from Gnumed.pycommon import gmI18N
26 from Gnumed.pycommon import gmMatchProvider
27 from Gnumed.pycommon import gmPG2
28 from Gnumed.pycommon import gmTools
29 from Gnumed.pycommon import gmCfg
30 from Gnumed.pycommon import gmDateTime
31 from Gnumed.pycommon import gmShellAPI
32 from Gnumed.pycommon import gmNetworkTools
33
34 from Gnumed.business import gmDemographicRecord
35 from Gnumed.business import gmPersonSearch
36 from Gnumed.business import gmSurgery
37 from Gnumed.business import gmPerson
38
39 from Gnumed.wxpython import gmPhraseWheel
40 from Gnumed.wxpython import gmRegetMixin
41 from Gnumed.wxpython import gmAuthWidgets
42 from Gnumed.wxpython import gmPersonContactWidgets
43 from Gnumed.wxpython import gmEditArea
44 from Gnumed.wxpython import gmListWidgets
45 from Gnumed.wxpython import gmDateTimeInput
46 from Gnumed.wxpython import gmDataMiningWidgets
47 from Gnumed.wxpython import gmGuiHelpers
48
49
50 # constant defs
51 _log = logging.getLogger('gm.ui')
52
53
54 try:
55 _('dummy-no-need-to-translate-but-make-epydoc-happy')
56 except NameError:
57 _ = lambda x:x
58
59 #============================================================
60 # image tags related widgets
61 #------------------------------------------------------------
63 if tag_image is not None:
64 if tag_image['is_in_use']:
65 gmGuiHelpers.gm_show_info (
66 aTitle = _('Editing tag'),
67 aMessage = _(
68 'Cannot edit the image tag\n'
69 '\n'
70 ' "%s"\n'
71 '\n'
72 'because it is currently in use.\n'
73 ) % tag_image['l10n_description']
74 )
75 return False
76
77 ea = cTagImageEAPnl(parent = parent, id = -1)
78 ea.data = tag_image
79 ea.mode = gmTools.coalesce(tag_image, 'new', 'edit')
80 dlg = gmEditArea.cGenericEditAreaDlg2(parent = parent, id = -1, edit_area = ea, single_entry = single_entry)
81 dlg.SetTitle(gmTools.coalesce(tag_image, _('Adding new tag'), _('Editing tag')))
82 if dlg.ShowModal() == wx.ID_OK:
83 dlg.Destroy()
84 return True
85 dlg.Destroy()
86 return False
87 #------------------------------------------------------------
89
90 if parent is None:
91 parent = wx.GetApp().GetTopWindow()
92 #------------------------------------------------------------
93 def go_to_openclipart_org(tag_image):
94 gmNetworkTools.open_url_in_browser(url = u'http://www.openclipart.org')
95 gmNetworkTools.open_url_in_browser(url = u'http://www.google.com')
96 return True
97 #------------------------------------------------------------
98 def edit(tag_image=None):
99 return edit_tag_image(parent = parent, tag_image = tag_image, single_entry = (tag_image is not None))
100 #------------------------------------------------------------
101 def delete(tag):
102 if tag['is_in_use']:
103 gmDispatcher.send(signal = 'statustext', msg = _('Cannot delete this tag. It is in use.'), beep = True)
104 return False
105
106 return gmDemographicRecord.delete_tag_image(tag_image = tag['pk_tag_image'])
107 #------------------------------------------------------------
108 def refresh(lctrl):
109 tags = gmDemographicRecord.get_tag_images(order_by = u'l10n_description')
110 items = [ [
111 t['l10n_description'],
112 gmTools.bool2subst(t['is_in_use'], u'X', u''),
113 u'%s' % t['size'],
114 t['pk_tag_image']
115 ] for t in tags ]
116 lctrl.set_string_items(items)
117 lctrl.set_column_widths(widths = [wx.LIST_AUTOSIZE, wx.LIST_AUTOSIZE_USEHEADER, wx.LIST_AUTOSIZE_USEHEADER, wx.LIST_AUTOSIZE])
118 lctrl.set_data(tags)
119 #------------------------------------------------------------
120 msg = _('\nTags with images registered with GNUmed.\n')
121
122 tag = gmListWidgets.get_choices_from_list (
123 parent = parent,
124 msg = msg,
125 caption = _('Showing tags with images.'),
126 columns = [_('Tag name'), _('In use'), _('Image size'), u'#'],
127 single_selection = True,
128 new_callback = edit,
129 edit_callback = edit,
130 delete_callback = delete,
131 refresh_callback = refresh,
132 left_extra_button = (_('WWW'), _('Go to www.openclipart.org for images.'), go_to_openclipart_org)
133 )
134
135 return tag
136 #------------------------------------------------------------
137 from Gnumed.wxGladeWidgets import wxgTagImageEAPnl
138
140
142
143 try:
144 data = kwargs['tag_image']
145 del kwargs['tag_image']
146 except KeyError:
147 data = None
148
149 wxgTagImageEAPnl.wxgTagImageEAPnl.__init__(self, *args, **kwargs)
150 gmEditArea.cGenericEditAreaMixin.__init__(self)
151
152 self.mode = 'new'
153 self.data = data
154 if data is not None:
155 self.mode = 'edit'
156
157 self.__selected_image_file = None
158 #----------------------------------------------------------------
159 # generic Edit Area mixin API
160 #----------------------------------------------------------------
162
163 valid = True
164
165 if self.mode == u'new':
166 if self.__selected_image_file is None:
167 valid = False
168 gmDispatcher.send(signal = 'statustext', msg = _('Must pick an image file for a new tag.'), beep = True)
169 self._BTN_pick_image.SetFocus()
170
171 if self.__selected_image_file is not None:
172 try:
173 open(self.__selected_image_file).close()
174 except StandardError:
175 valid = False
176 self.__selected_image_file = None
177 gmDispatcher.send(signal = 'statustext', msg = _('Cannot open the image file [%s].') % self.__selected_image_file, beep = True)
178 self._BTN_pick_image.SetFocus()
179
180 if self._TCTRL_description.GetValue().strip() == u'':
181 valid = False
182 self.display_tctrl_as_valid(self._TCTRL_description, False)
183 self._TCTRL_description.SetFocus()
184 else:
185 self.display_tctrl_as_valid(self._TCTRL_description, True)
186
187 return (valid is True)
188 #----------------------------------------------------------------
190
191 dbo_conn = gmAuthWidgets.get_dbowner_connection(procedure = _('Creating tag with image'))
192 if dbo_conn is None:
193 return False
194
195 data = gmDemographicRecord.create_tag_image(description = self._TCTRL_description.GetValue().strip(), link_obj = dbo_conn)
196 dbo_conn.close()
197
198 data['filename'] = self._TCTRL_filename.GetValue().strip()
199 data.save()
200 data.update_image_from_file(filename = self.__selected_image_file)
201
202 # must be done very late or else the property access
203 # will refresh the display such that later field
204 # access will return empty values
205 self.data = data
206 return True
207 #----------------------------------------------------------------
209
210 # this is somewhat fake as it never actually uses the gm-dbo conn
211 # (although it does verify it)
212 dbo_conn = gmAuthWidgets.get_dbowner_connection(procedure = _('Updating tag with image'))
213 if dbo_conn is None:
214 return False
215 dbo_conn.close()
216
217 self.data['description'] = self._TCTRL_description.GetValue().strip()
218 self.data['filename'] = self._TCTRL_filename.GetValue().strip()
219 self.data.save()
220
221 if self.__selected_image_file is not None:
222 open(self.__selected_image_file).close()
223 self.data.update_image_from_file(filename = self.__selected_image_file)
224 self.__selected_image_file = None
225
226 return True
227 #----------------------------------------------------------------
229 self._TCTRL_description.SetValue(u'')
230 self._TCTRL_filename.SetValue(u'')
231 self._BMP_image.SetBitmap(bitmap = wx.EmptyBitmap(100, 100))
232
233 self.__selected_image_file = None
234
235 self._TCTRL_description.SetFocus()
236 #----------------------------------------------------------------
239 #----------------------------------------------------------------
241 self._TCTRL_description.SetValue(self.data['l10n_description'])
242 self._TCTRL_filename.SetValue(gmTools.coalesce(self.data['filename'], u''))
243 fname = self.data.export_image2file()
244 if fname is None:
245 self._BMP_image.SetBitmap(bitmap = wx.EmptyBitmap(100, 100))
246 else:
247 self._BMP_image.SetBitmap(bitmap = gmGuiHelpers.file2scaled_image(filename = fname, height = 100))
248
249 self.__selected_image_file = None
250
251 self._TCTRL_description.SetFocus()
252 #----------------------------------------------------------------
253 # event handlers
254 #----------------------------------------------------------------
266
267 #============================================================
268 from Gnumed.wxGladeWidgets import wxgVisualSoapPresenterPnl
269
271
273 wxgVisualSoapPresenterPnl.wxgVisualSoapPresenterPnl.__init__(self, *args, **kwargs)
274 self._SZR_bitmaps = self.GetSizer()
275 self.__bitmaps = []
276
277 self.__context_popup = wx.Menu()
278
279 item = self.__context_popup.Append(-1, _('&Edit comment'))
280 self.Bind(wx.EVT_MENU, self.__edit_tag, item)
281
282 item = self.__context_popup.Append(-1, _('&Remove tag'))
283 self.Bind(wx.EVT_MENU, self.__remove_tag, item)
284 #--------------------------------------------------------
285 # external API
286 #--------------------------------------------------------
288
289 self.clear()
290
291 for tag in patient.get_tags(order_by = u'l10n_description'):
292 fname = tag.export_image2file()
293 if fname is None:
294 _log.warning('cannot export image data of tag [%s]', tag['l10n_description'])
295 continue
296 img = gmGuiHelpers.file2scaled_image(filename = fname, height = 20)
297 bmp = wx_genstatbmp.GenStaticBitmap(self, -1, img, style = wx.NO_BORDER)
298 bmp.SetToolTipString(u'%s%s' % (
299 tag['l10n_description'],
300 gmTools.coalesce(tag['comment'], u'', u'\n\n%s')
301 ))
302 bmp.tag = tag
303 bmp.Bind(wx.EVT_RIGHT_UP, self._on_bitmap_rightclicked)
304 # FIXME: add context menu for Delete/Clone/Add/Configure
305 self._SZR_bitmaps.Add(bmp, 0, wx.LEFT | wx.RIGHT | wx.TOP | wx.BOTTOM, 1) # | wx.EXPAND
306 self.__bitmaps.append(bmp)
307
308 self.GetParent().Layout()
309 #--------------------------------------------------------
311 while len(self._SZR_bitmaps.GetChildren()) > 0:
312 self._SZR_bitmaps.Detach(0)
313 # for child_idx in range(len(self._SZR_bitmaps.GetChildren())):
314 # self._SZR_bitmaps.Detach(child_idx)
315 for bmp in self.__bitmaps:
316 bmp.Destroy()
317 self.__bitmaps = []
318 #--------------------------------------------------------
319 # internal helpers
320 #--------------------------------------------------------
322 if self.__current_tag is None:
323 return
324 pat = gmPerson.gmCurrentPatient()
325 if not pat.connected:
326 return
327 pat.remove_tag(tag = self.__current_tag['pk_identity_tag'])
328 #--------------------------------------------------------
330 if self.__current_tag is None:
331 return
332
333 msg = _('Edit the comment on tag [%s]') % self.__current_tag['l10n_description']
334 comment = wx.GetTextFromUser (
335 message = msg,
336 caption = _('Editing tag comment'),
337 default_value = gmTools.coalesce(self.__current_tag['comment'], u''),
338 parent = self
339 )
340
341 if comment == u'':
342 return
343
344 if comment.strip() == self.__current_tag['comment']:
345 return
346
347 if comment == u' ':
348 self.__current_tag['comment'] = None
349 else:
350 self.__current_tag['comment'] = comment.strip()
351
352 self.__current_tag.save()
353 #--------------------------------------------------------
354 # event handlers
355 #--------------------------------------------------------
360 #============================================================
361 #============================================================
363
365
366 kwargs['message'] = _("Today's KOrganizer appointments ...")
367 kwargs['button_defs'] = [
368 {'label': _('Reload'), 'tooltip': _('Reload appointments from KOrganizer')},
369 {'label': u''},
370 {'label': u''},
371 {'label': u''},
372 {'label': u'KOrganizer', 'tooltip': _('Launch KOrganizer')}
373 ]
374 gmDataMiningWidgets.cPatientListingPnl.__init__(self, *args, **kwargs)
375
376 self.fname = os.path.expanduser(os.path.join('~', '.gnumed', 'tmp', 'korganizer2gnumed.csv'))
377 self.reload_cmd = 'konsolekalendar --view --export-type csv --export-file %s' % self.fname
378
379 #--------------------------------------------------------
383 #--------------------------------------------------------
385 """Reload appointments from KOrganizer."""
386 found, cmd = gmShellAPI.detect_external_binary(binary = 'korganizer')
387
388 if not found:
389 gmDispatcher.send(signal = 'statustext', msg = _('KOrganizer is not installed.'), beep = True)
390 return
391
392 gmShellAPI.run_command_in_shell(command = cmd, blocking = False)
393 #--------------------------------------------------------
395 try: os.remove(self.fname)
396 except OSError: pass
397 gmShellAPI.run_command_in_shell(command=self.reload_cmd, blocking=True)
398 try:
399 csv_file = codecs.open(self.fname , mode = 'rU', encoding = 'utf8', errors = 'replace')
400 except IOError:
401 gmDispatcher.send(signal = u'statustext', msg = _('Cannot access KOrganizer transfer file [%s]') % self.fname, beep = True)
402 return
403
404 csv_lines = gmTools.unicode_csv_reader (
405 csv_file,
406 delimiter = ','
407 )
408 # start_date, start_time, end_date, end_time, title (patient), ort, comment, UID
409 self._LCTRL_items.set_columns ([
410 _('Place'),
411 _('Start'),
412 u'',
413 u'',
414 _('Patient'),
415 _('Comment')
416 ])
417 items = []
418 data = []
419 for line in csv_lines:
420 items.append([line[5], line[0], line[1], line[3], line[4], line[6]])
421 data.append([line[4], line[7]])
422
423 self._LCTRL_items.set_string_items(items = items)
424 self._LCTRL_items.set_column_widths()
425 self._LCTRL_items.set_data(data = data)
426 self._LCTRL_items.patient_key = 0
427 #--------------------------------------------------------
428 # notebook plugins API
429 #--------------------------------------------------------
431 self.reload_appointments()
432 #============================================================
433 # occupation related widgets / functions
434 #============================================================
436
437 pat = gmPerson.gmCurrentPatient()
438 curr_jobs = pat.get_occupations()
439 if len(curr_jobs) > 0:
440 old_job = curr_jobs[0]['l10n_occupation']
441 update = curr_jobs[0]['modified_when'].strftime('%m/%Y')
442 else:
443 old_job = u''
444 update = u''
445
446 msg = _(
447 'Please enter the primary occupation of the patient.\n'
448 '\n'
449 'Currently recorded:\n'
450 '\n'
451 ' %s (last updated %s)'
452 ) % (old_job, update)
453
454 new_job = wx.GetTextFromUser (
455 message = msg,
456 caption = _('Editing primary occupation'),
457 default_value = old_job,
458 parent = None
459 )
460 if new_job.strip() == u'':
461 return
462
463 for job in curr_jobs:
464 # unlink all but the new job
465 if job['l10n_occupation'] != new_job:
466 pat.unlink_occupation(occupation = job['l10n_occupation'])
467 # and link the new one
468 pat.link_occupation(occupation = new_job)
469
470 #------------------------------------------------------------
472
474 query = u"SELECT distinct name, _(name) from dem.occupation where _(name) %(fragment_condition)s"
475 mp = gmMatchProvider.cMatchProvider_SQL2(queries=query)
476 mp.setThresholds(1, 3, 5)
477 gmPhraseWheel.cPhraseWheel.__init__ (
478 self,
479 *args,
480 **kwargs
481 )
482 self.SetToolTipString(_("Type or select an occupation."))
483 self.capitalisation_mode = gmTools.CAPS_FIRST
484 self.matcher = mp
485
486 #============================================================
487 # identity widgets / functions
488 #============================================================
490 # ask user for assurance
491 go_ahead = gmGuiHelpers.gm_show_question (
492 _('Are you sure you really, positively want\n'
493 'to disable the following person ?\n'
494 '\n'
495 ' %s %s %s\n'
496 ' born %s\n'
497 '\n'
498 '%s\n'
499 ) % (
500 identity['firstnames'],
501 identity['lastnames'],
502 identity['gender'],
503 identity['dob'],
504 gmTools.bool2subst (
505 identity.is_patient,
506 _('This patient DID receive care.'),
507 _('This person did NOT receive care.')
508 )
509 ),
510 _('Disabling person')
511 )
512 if not go_ahead:
513 return True
514
515 # get admin connection
516 conn = gmAuthWidgets.get_dbowner_connection (
517 procedure = _('Disabling patient')
518 )
519 # - user cancelled
520 if conn is False:
521 return True
522 # - error
523 if conn is None:
524 return False
525
526 # now disable patient
527 gmPG2.run_rw_queries(queries = [{'cmd': u"update dem.identity set deleted=True where pk=%s", 'args': [identity['pk_identity']]}])
528
529 return True
530
531 #------------------------------------------------------------
532 # phrasewheels
533 #------------------------------------------------------------
535
537 query = u"SELECT distinct lastnames, lastnames from dem.names where lastnames %(fragment_condition)s order by lastnames limit 25"
538 mp = gmMatchProvider.cMatchProvider_SQL2(queries=query)
539 mp.setThresholds(3, 5, 9)
540 gmPhraseWheel.cPhraseWheel.__init__ (
541 self,
542 *args,
543 **kwargs
544 )
545 self.SetToolTipString(_("Type or select a last name (family name/surname)."))
546 self.capitalisation_mode = gmTools.CAPS_NAMES
547 self.matcher = mp
548 #------------------------------------------------------------
550
552 query = u"""
553 (SELECT distinct firstnames, firstnames from dem.names where firstnames %(fragment_condition)s order by firstnames limit 20)
554 union
555 (SELECT distinct name, name from dem.name_gender_map where name %(fragment_condition)s order by name limit 20)"""
556 mp = gmMatchProvider.cMatchProvider_SQL2(queries=query)
557 mp.setThresholds(3, 5, 9)
558 gmPhraseWheel.cPhraseWheel.__init__ (
559 self,
560 *args,
561 **kwargs
562 )
563 self.SetToolTipString(_("Type or select a first name (forename/Christian name/given name)."))
564 self.capitalisation_mode = gmTools.CAPS_NAMES
565 self.matcher = mp
566 #------------------------------------------------------------
568
570 query = u"""
571 (SELECT distinct preferred, preferred from dem.names where preferred %(fragment_condition)s order by preferred limit 20)
572 union
573 (SELECT distinct firstnames, firstnames from dem.names where firstnames %(fragment_condition)s order by firstnames limit 20)
574 union
575 (SELECT distinct name, name from dem.name_gender_map where name %(fragment_condition)s order by name limit 20)"""
576 mp = gmMatchProvider.cMatchProvider_SQL2(queries=query)
577 mp.setThresholds(3, 5, 9)
578 gmPhraseWheel.cPhraseWheel.__init__ (
579 self,
580 *args,
581 **kwargs
582 )
583 self.SetToolTipString(_("Type or select an alias (nick name, preferred name, call name, warrior name, artist name)."))
584 # nicknames CAN start with lower case !
585 #self.capitalisation_mode = gmTools.CAPS_NAMES
586 self.matcher = mp
587 #------------------------------------------------------------
589
591 query = u"SELECT distinct title, title from dem.identity where title %(fragment_condition)s"
592 mp = gmMatchProvider.cMatchProvider_SQL2(queries=query)
593 mp.setThresholds(1, 3, 9)
594 gmPhraseWheel.cPhraseWheel.__init__ (
595 self,
596 *args,
597 **kwargs
598 )
599 self.SetToolTipString(_("Type or select a title. Note that the title applies to the person, not to a particular name !"))
600 self.matcher = mp
601 #------------------------------------------------------------
603 """Let user select a gender."""
604
605 _gender_map = None
606
608
609 if cGenderSelectionPhraseWheel._gender_map is None:
610 cmd = u"""
611 SELECT tag, l10n_label, sort_weight
612 from dem.v_gender_labels
613 order by sort_weight desc"""
614 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd}], get_col_idx=True)
615 cGenderSelectionPhraseWheel._gender_map = {}
616 for gender in rows:
617 cGenderSelectionPhraseWheel._gender_map[gender[idx['tag']]] = {
618 'data': gender[idx['tag']],
619 'field_label': gender[idx['l10n_label']],
620 'list_label': gender[idx['l10n_label']],
621 'weight': gender[idx['sort_weight']]
622 }
623
624 mp = gmMatchProvider.cMatchProvider_FixedList(aSeq = cGenderSelectionPhraseWheel._gender_map.values())
625 mp.setThresholds(1, 1, 3)
626
627 gmPhraseWheel.cPhraseWheel.__init__(self, *args, **kwargs)
628 self.selection_only = True
629 self.matcher = mp
630 self.picklist_delay = 50
631 #------------------------------------------------------------
633
635 query = u"""
636 SELECT DISTINCT ON (list_label)
637 pk AS data,
638 name AS field_label,
639 name || coalesce(' (' || issuer || ')', '') as list_label
640 FROM dem.enum_ext_id_types
641 WHERE name %(fragment_condition)s
642 ORDER BY list_label
643 LIMIT 25
644 """
645 mp = gmMatchProvider.cMatchProvider_SQL2(queries=query)
646 mp.setThresholds(1, 3, 5)
647 gmPhraseWheel.cPhraseWheel.__init__(self, *args, **kwargs)
648 self.SetToolTipString(_("Enter or select a type for the external ID."))
649 self.matcher = mp
650 #--------------------------------------------------------
655 #------------------------------------------------------------
657
659 query = u"""
660 SELECT distinct issuer, issuer
661 from dem.enum_ext_id_types
662 where issuer %(fragment_condition)s
663 order by issuer limit 25"""
664 mp = gmMatchProvider.cMatchProvider_SQL2(queries=query)
665 mp.setThresholds(1, 3, 5)
666 gmPhraseWheel.cPhraseWheel.__init__(self, *args, **kwargs)
667 self.SetToolTipString(_("Type or select an ID issuer."))
668 self.capitalisation_mode = gmTools.CAPS_FIRST
669 self.matcher = mp
670 #------------------------------------------------------------
671 # edit areas
672 #------------------------------------------------------------
673 from Gnumed.wxGladeWidgets import wxgExternalIDEditAreaPnl
674
675 -class cExternalIDEditAreaPnl(wxgExternalIDEditAreaPnl.wxgExternalIDEditAreaPnl, gmEditArea.cGenericEditAreaMixin):
676 """An edit area for editing/creating external IDs.
677
678 Does NOT act on/listen to the current patient.
679 """
681
682 try:
683 data = kwargs['external_id']
684 del kwargs['external_id']
685 except:
686 data = None
687
688 wxgExternalIDEditAreaPnl.wxgExternalIDEditAreaPnl.__init__(self, *args, **kwargs)
689 gmEditArea.cGenericEditAreaMixin.__init__(self)
690
691 self.identity = None
692
693 self.mode = 'new'
694 self.data = data
695 if data is not None:
696 self.mode = 'edit'
697
698 self.__init_ui()
699 #--------------------------------------------------------
701 self._PRW_type.add_callback_on_lose_focus(self._on_type_set)
702 #----------------------------------------------------------------
703 # generic Edit Area mixin API
704 #----------------------------------------------------------------
706 validity = True
707
708 # do not test .GetData() because adding external
709 # IDs will create types as necessary
710 #if self._PRW_type.GetData() is None:
711 if self._PRW_type.GetValue().strip() == u'':
712 validity = False
713 self._PRW_type.display_as_valid(False)
714 self._PRW_type.SetFocus()
715 else:
716 self._PRW_type.display_as_valid(True)
717
718 if self._TCTRL_value.GetValue().strip() == u'':
719 validity = False
720 self.display_tctrl_as_valid(tctrl = self._TCTRL_value, valid = False)
721 else:
722 self.display_tctrl_as_valid(tctrl = self._TCTRL_value, valid = True)
723
724 return validity
725 #----------------------------------------------------------------
727 data = {}
728 data['pk_type'] = None
729 data['name'] = self._PRW_type.GetValue().strip()
730 data['value'] = self._TCTRL_value.GetValue().strip()
731 data['issuer'] = gmTools.none_if(self._PRW_issuer.GetValue().strip(), u'')
732 data['comment'] = gmTools.none_if(self._TCTRL_comment.GetValue().strip(), u'')
733
734 self.identity.add_external_id (
735 type_name = data['name'],
736 value = data['value'],
737 issuer = data['issuer'],
738 comment = data['comment']
739 )
740
741 self.data = data
742 return True
743 #----------------------------------------------------------------
745 self.data['name'] = self._PRW_type.GetValue().strip()
746 self.data['value'] = self._TCTRL_value.GetValue().strip()
747 self.data['issuer'] = gmTools.none_if(self._PRW_issuer.GetValue().strip(), u'')
748 self.data['comment'] = gmTools.none_if(self._TCTRL_comment.GetValue().strip(), u'')
749
750 self.identity.update_external_id (
751 pk_id = self.data['pk_id'],
752 type = self.data['name'],
753 value = self.data['value'],
754 issuer = self.data['issuer'],
755 comment = self.data['comment']
756 )
757
758 return True
759 #----------------------------------------------------------------
761 self._PRW_type.SetText(value = u'', data = None)
762 self._TCTRL_value.SetValue(u'')
763 self._PRW_issuer.SetText(value = u'', data = None)
764 self._TCTRL_comment.SetValue(u'')
765 #----------------------------------------------------------------
769 #----------------------------------------------------------------
771 self._PRW_type.SetText(value = self.data['name'], data = self.data['pk_type'])
772 self._TCTRL_value.SetValue(self.data['value'])
773 self._PRW_issuer.SetText(self.data['issuer'])
774 self._TCTRL_comment.SetValue(gmTools.coalesce(self.data['comment'], u''))
775 #----------------------------------------------------------------
776 # internal helpers
777 #----------------------------------------------------------------
779 """Set the issuer according to the selected type.
780
781 Matches are fetched from existing records in backend.
782 """
783 pk_curr_type = self._PRW_type.GetData()
784 if pk_curr_type is None:
785 return True
786 rows, idx = gmPG2.run_ro_queries(queries = [{
787 'cmd': u"SELECT issuer from dem.enum_ext_id_types where pk = %s",
788 'args': [pk_curr_type]
789 }])
790 if len(rows) == 0:
791 return True
792 wx.CallAfter(self._PRW_issuer.SetText, rows[0][0])
793 return True
794
795 #============================================================
796 # identity widgets
797 #------------------------------------------------------------
799 allow_empty_dob = gmGuiHelpers.gm_show_question (
800 _(
801 'Are you sure you want to leave this person\n'
802 'without a valid date of birth ?\n'
803 '\n'
804 'This can be useful for temporary staff members\n'
805 'but will provoke nag screens if this person\n'
806 'becomes a patient.\n'
807 ),
808 _('Validating date of birth')
809 )
810 return allow_empty_dob
811 #------------------------------------------------------------
813
814 # valid timestamp ?
815 if dob_prw.is_valid_timestamp(allow_empty = False): # properly colors the field
816 dob = dob_prw.date
817 # but year also usable ?
818 if (dob.year > 1899) and (dob < gmDateTime.pydt_now_here()):
819 return True
820
821 if dob.year < 1900:
822 msg = _(
823 'DOB: %s\n'
824 '\n'
825 'While this is a valid point in time Python does\n'
826 'not know how to deal with it.\n'
827 '\n'
828 'We suggest using January 1st 1901 instead and adding\n'
829 'the true date of birth to the patient comment.\n'
830 '\n'
831 'Sorry for the inconvenience %s'
832 ) % (dob, gmTools.u_frowning_face)
833 else:
834 msg = _(
835 'DOB: %s\n'
836 '\n'
837 'Date of birth in the future !'
838 ) % dob
839 gmGuiHelpers.gm_show_error (
840 msg,
841 _('Validating date of birth')
842 )
843 dob_prw.display_as_valid(False)
844 dob_prw.SetFocus()
845 return False
846
847 # invalid timestamp but not empty
848 if dob_prw.GetValue().strip() != u'':
849 dob_prw.display_as_valid(False)
850 gmDispatcher.send(signal = u'statustext', msg = _('Invalid date of birth.'))
851 dob_prw.SetFocus()
852 return False
853
854 # empty DOB field
855 dob_prw.display_as_valid(False)
856 return True
857 #------------------------------------------------------------
858 from Gnumed.wxGladeWidgets import wxgIdentityEAPnl
859
861 """An edit area for editing/creating title/gender/dob/dod etc."""
862
864
865 try:
866 data = kwargs['identity']
867 del kwargs['identity']
868 except KeyError:
869 data = None
870
871 wxgIdentityEAPnl.wxgIdentityEAPnl.__init__(self, *args, **kwargs)
872 gmEditArea.cGenericEditAreaMixin.__init__(self)
873
874 self.mode = 'new'
875 self.data = data
876 if data is not None:
877 self.mode = 'edit'
878
879 # self.__init_ui()
880 #----------------------------------------------------------------
881 # def __init_ui(self):
882 # # adjust phrasewheels etc
883 #----------------------------------------------------------------
884 # generic Edit Area mixin API
885 #----------------------------------------------------------------
887
888 has_error = False
889
890 if self._PRW_gender.GetData() is None:
891 self._PRW_gender.SetFocus()
892 has_error = True
893
894 if self.data is not None:
895 if not _validate_dob_field(self._PRW_dob):
896 has_error = True
897
898 if not self._PRW_dod.is_valid_timestamp(allow_empty = True):
899 gmDispatcher.send(signal = u'statustext', msg = _('Invalid date of death.'))
900 self._PRW_dod.SetFocus()
901 has_error = True
902
903 return (has_error is False)
904 #----------------------------------------------------------------
908 #----------------------------------------------------------------
910
911 if self._PRW_dob.GetValue().strip() == u'':
912 if not _empty_dob_allowed():
913 return False
914 self.data['dob'] = None
915 else:
916 self.data['dob'] = self._PRW_dob.GetData()
917
918 self.data['gender'] = self._PRW_gender.GetData()
919 self.data['title'] = gmTools.none_if(self._PRW_title.GetValue().strip(), u'')
920 self.data['deceased'] = self._PRW_dod.GetData()
921 self.data['comment'] = gmTools.none_if(self._TCTRL_comment.GetValue().strip(), u'')
922
923 self.data.save()
924 return True
925 #----------------------------------------------------------------
928 #----------------------------------------------------------------
930
931 self._LBL_info.SetLabel(u'ID: #%s' % (
932 self.data.ID
933 # FIXME: add 'deleted' status
934 ))
935 if self.data['dob'] is None:
936 val = u''
937 else:
938 val = gmDateTime.pydt_strftime (
939 self.data['dob'],
940 format = '%Y-%m-%d %H:%M',
941 accuracy = gmDateTime.acc_minutes
942 )
943 self._PRW_dob.SetText(value = val, data = self.data['dob'])
944 if self.data['deceased'] is None:
945 val = u''
946 else:
947 val = gmDateTime.pydt_strftime (
948 self.data['deceased'],
949 format = '%Y-%m-%d %H:%M',
950 accuracy = gmDateTime.acc_minutes
951 )
952 self._PRW_dod.SetText(value = val, data = self.data['deceased'])
953 self._PRW_gender.SetData(self.data['gender'])
954 #self._PRW_ethnicity.SetValue()
955 self._PRW_title.SetText(gmTools.coalesce(self.data['title'], u''))
956 self._TCTRL_comment.SetValue(gmTools.coalesce(self.data['comment'], u''))
957 #----------------------------------------------------------------
960 #------------------------------------------------------------
961 from Gnumed.wxGladeWidgets import wxgPersonNameEAPnl
962
963 -class cPersonNameEAPnl(wxgPersonNameEAPnl.wxgPersonNameEAPnl, gmEditArea.cGenericEditAreaMixin):
964 """An edit area for editing/creating names of people.
965
966 Does NOT act on/listen to the current patient.
967 """
969
970 try:
971 data = kwargs['name']
972 identity = gmPerson.cIdentity(aPK_obj = data['pk_identity'])
973 del kwargs['name']
974 except KeyError:
975 data = None
976 identity = kwargs['identity']
977 del kwargs['identity']
978
979 wxgPersonNameEAPnl.wxgPersonNameEAPnl.__init__(self, *args, **kwargs)
980 gmEditArea.cGenericEditAreaMixin.__init__(self)
981
982 self.__identity = identity
983
984 self.mode = 'new'
985 self.data = data
986 if data is not None:
987 self.mode = 'edit'
988
989 #self.__init_ui()
990 #----------------------------------------------------------------
991 # def __init_ui(self):
992 # # adjust phrasewheels etc
993 #----------------------------------------------------------------
994 # generic Edit Area mixin API
995 #----------------------------------------------------------------
997 validity = True
998
999 if self._PRW_lastname.GetValue().strip() == u'':
1000 validity = False
1001 self._PRW_lastname.display_as_valid(False)
1002 self._PRW_lastname.SetFocus()
1003 else:
1004 self._PRW_lastname.display_as_valid(True)
1005
1006 if self._PRW_firstname.GetValue().strip() == u'':
1007 validity = False
1008 self._PRW_firstname.display_as_valid(False)
1009 self._PRW_firstname.SetFocus()
1010 else:
1011 self._PRW_firstname.display_as_valid(True)
1012
1013 return validity
1014 #----------------------------------------------------------------
1016
1017 first = self._PRW_firstname.GetValue().strip()
1018 last = self._PRW_lastname.GetValue().strip()
1019 active = self._CHBOX_active.GetValue()
1020
1021 data = self.__identity.add_name(first, last, active)
1022
1023 old_nick = self.__identity['active_name']['preferred']
1024 new_nick = gmTools.none_if(self._PRW_nick.GetValue().strip(), u'')
1025 if active:
1026 data['preferred'] = gmTools.coalesce(new_nick, old_nick)
1027 else:
1028 data['preferred'] = new_nick
1029 data['comment'] = gmTools.none_if(self._TCTRL_comment.GetValue().strip(), u'')
1030 data.save()
1031
1032 self.data = data
1033 return True
1034 #----------------------------------------------------------------
1036 """The knack here is that we can only update a few fields.
1037
1038 Otherwise we need to clone the name and update that.
1039 """
1040 first = self._PRW_firstname.GetValue().strip()
1041 last = self._PRW_lastname.GetValue().strip()
1042 active = self._CHBOX_active.GetValue()
1043
1044 current_name = self.data['firstnames'].strip() + self.data['lastnames'].strip()
1045 new_name = first + last
1046
1047 # editable fields only ?
1048 if new_name == current_name:
1049 self.data['active_name'] = self._CHBOX_active.GetValue()
1050 self.data['preferred'] = gmTools.none_if(self._PRW_nick.GetValue().strip(), u'')
1051 self.data['comment'] = gmTools.none_if(self._TCTRL_comment.GetValue().strip(), u'')
1052 self.data.save()
1053 # else clone name and update that
1054 else:
1055 name = self.__identity.add_name(first, last, active)
1056 name['preferred'] = gmTools.none_if(self._PRW_nick.GetValue().strip(), u'')
1057 name['comment'] = gmTools.none_if(self._TCTRL_comment.GetValue().strip(), u'')
1058 name.save()
1059 self.data = name
1060
1061 return True
1062 #----------------------------------------------------------------
1064 self._PRW_firstname.SetText(value = u'', data = None)
1065 self._PRW_lastname.SetText(value = u'', data = None)
1066 self._PRW_nick.SetText(value = u'', data = None)
1067 self._TCTRL_comment.SetValue(u'')
1068 self._CHBOX_active.SetValue(False)
1069
1070 self._PRW_firstname.SetFocus()
1071 #----------------------------------------------------------------
1073 self._refresh_as_new()
1074 self._PRW_firstname.SetText(value = u'', data = None)
1075 self._PRW_nick.SetText(gmTools.coalesce(self.data['preferred'], u''))
1076
1077 self._PRW_lastname.SetFocus()
1078 #----------------------------------------------------------------
1080 self._PRW_firstname.SetText(self.data['firstnames'])
1081 self._PRW_lastname.SetText(self.data['lastnames'])
1082 self._PRW_nick.SetText(gmTools.coalesce(self.data['preferred'], u''))
1083 self._TCTRL_comment.SetValue(gmTools.coalesce(self.data['comment'], u''))
1084 self._CHBOX_active.SetValue(self.data['active_name'])
1085
1086 self._TCTRL_comment.SetFocus()
1087 #------------------------------------------------------------
1088 # list manager
1089 #------------------------------------------------------------
1091 """A list for managing a person's names.
1092
1093 Does NOT act on/listen to the current patient.
1094 """
1096
1097 try:
1098 self.__identity = kwargs['identity']
1099 del kwargs['identity']
1100 except KeyError:
1101 self.__identity = None
1102
1103 gmListWidgets.cGenericListManagerPnl.__init__(self, *args, **kwargs)
1104
1105 self.new_callback = self._add_name
1106 self.edit_callback = self._edit_name
1107 self.delete_callback = self._del_name
1108 self.refresh_callback = self.refresh
1109
1110 self.__init_ui()
1111 self.refresh()
1112 #--------------------------------------------------------
1113 # external API
1114 #--------------------------------------------------------
1116 if self.__identity is None:
1117 self._LCTRL_items.set_string_items()
1118 return
1119
1120 names = self.__identity.get_names()
1121 self._LCTRL_items.set_string_items (
1122 items = [ [
1123 gmTools.bool2str(n['active_name'], 'X', ''),
1124 n['lastnames'],
1125 n['firstnames'],
1126 gmTools.coalesce(n['preferred'], u''),
1127 gmTools.coalesce(n['comment'], u'')
1128 ] for n in names ]
1129 )
1130 self._LCTRL_items.set_column_widths()
1131 self._LCTRL_items.set_data(data = names)
1132 #--------------------------------------------------------
1133 # internal helpers
1134 #--------------------------------------------------------
1136 self._LCTRL_items.set_columns(columns = [
1137 _('Active'),
1138 _('Lastname'),
1139 _('Firstname(s)'),
1140 _('Preferred Name'),
1141 _('Comment')
1142 ])
1143 self._BTN_edit.SetLabel(_('Clone and &edit'))
1144 #--------------------------------------------------------
1146 #ea = cPersonNameEAPnl(self, -1, name = self.__identity.get_active_name())
1147 ea = cPersonNameEAPnl(self, -1, identity = self.__identity)
1148 dlg = gmEditArea.cGenericEditAreaDlg2(self, -1, edit_area = ea, single_entry = True)
1149 dlg.SetTitle(_('Adding new name'))
1150 if dlg.ShowModal() == wx.ID_OK:
1151 dlg.Destroy()
1152 return True
1153 dlg.Destroy()
1154 return False
1155 #--------------------------------------------------------
1157 ea = cPersonNameEAPnl(self, -1, name = name)
1158 dlg = gmEditArea.cGenericEditAreaDlg2(self, -1, edit_area = ea, single_entry = True)
1159 dlg.SetTitle(_('Cloning name'))
1160 if dlg.ShowModal() == wx.ID_OK:
1161 dlg.Destroy()
1162 return True
1163 dlg.Destroy()
1164 return False
1165 #--------------------------------------------------------
1167
1168 if len(self.__identity.get_names()) == 1:
1169 gmDispatcher.send(signal = u'statustext', msg = _('Cannot delete the only name of a person.'), beep = True)
1170 return False
1171
1172 if name['active_name']:
1173 gmDispatcher.send(signal = u'statustext', msg = _('Cannot delete the active name of a person.'), beep = True)
1174 return False
1175
1176 go_ahead = gmGuiHelpers.gm_show_question (
1177 _( 'It is often advisable to keep old names around and\n'
1178 'just create a new "currently active" name.\n'
1179 '\n'
1180 'This allows finding the patient by both the old\n'
1181 'and the new name (think before/after marriage).\n'
1182 '\n'
1183 'Do you still want to really delete\n'
1184 "this name from the patient ?"
1185 ),
1186 _('Deleting name')
1187 )
1188 if not go_ahead:
1189 return False
1190
1191 self.__identity.delete_name(name = name)
1192 return True
1193 #--------------------------------------------------------
1194 # properties
1195 #--------------------------------------------------------
1198
1202
1203 identity = property(_get_identity, _set_identity)
1204 #------------------------------------------------------------
1206 """A list for managing a person's external IDs.
1207
1208 Does NOT act on/listen to the current patient.
1209 """
1211
1212 try:
1213 self.__identity = kwargs['identity']
1214 del kwargs['identity']
1215 except KeyError:
1216 self.__identity = None
1217
1218 gmListWidgets.cGenericListManagerPnl.__init__(self, *args, **kwargs)
1219
1220 self.new_callback = self._add_id
1221 self.edit_callback = self._edit_id
1222 self.delete_callback = self._del_id
1223 self.refresh_callback = self.refresh
1224
1225 self.__init_ui()
1226 self.refresh()
1227 #--------------------------------------------------------
1228 # external API
1229 #--------------------------------------------------------
1231 if self.__identity is None:
1232 self._LCTRL_items.set_string_items()
1233 return
1234
1235 ids = self.__identity.get_external_ids()
1236 self._LCTRL_items.set_string_items (
1237 items = [ [
1238 i['name'],
1239 i['value'],
1240 gmTools.coalesce(i['issuer'], u''),
1241 gmTools.coalesce(i['comment'], u'')
1242 ] for i in ids
1243 ]
1244 )
1245 self._LCTRL_items.set_column_widths()
1246 self._LCTRL_items.set_data(data = ids)
1247 #--------------------------------------------------------
1248 # internal helpers
1249 #--------------------------------------------------------
1251 self._LCTRL_items.set_columns(columns = [
1252 _('ID type'),
1253 _('Value'),
1254 _('Issuer'),
1255 _('Comment')
1256 ])
1257 #--------------------------------------------------------
1259 ea = cExternalIDEditAreaPnl(self, -1)
1260 ea.identity = self.__identity
1261 dlg = gmEditArea.cGenericEditAreaDlg2(self, -1, edit_area = ea)
1262 dlg.SetTitle(_('Adding new external ID'))
1263 if dlg.ShowModal() == wx.ID_OK:
1264 dlg.Destroy()
1265 return True
1266 dlg.Destroy()
1267 return False
1268 #--------------------------------------------------------
1270 ea = cExternalIDEditAreaPnl(self, -1, external_id = ext_id)
1271 ea.identity = self.__identity
1272 dlg = gmEditArea.cGenericEditAreaDlg2(self, -1, edit_area = ea, single_entry = True)
1273 dlg.SetTitle(_('Editing external ID'))
1274 if dlg.ShowModal() == wx.ID_OK:
1275 dlg.Destroy()
1276 return True
1277 dlg.Destroy()
1278 return False
1279 #--------------------------------------------------------
1281 go_ahead = gmGuiHelpers.gm_show_question (
1282 _( 'Do you really want to delete this\n'
1283 'external ID from the patient ?'),
1284 _('Deleting external ID')
1285 )
1286 if not go_ahead:
1287 return False
1288 self.__identity.delete_external_id(pk_ext_id = ext_id['pk_id'])
1289 return True
1290 #--------------------------------------------------------
1291 # properties
1292 #--------------------------------------------------------
1295
1299
1300 identity = property(_get_identity, _set_identity)
1301 #------------------------------------------------------------
1302 # integrated panels
1303 #------------------------------------------------------------
1304 from Gnumed.wxGladeWidgets import wxgPersonIdentityManagerPnl
1305
1307 """A panel for editing identity data for a person.
1308
1309 - provides access to:
1310 - identity EA
1311 - name list manager
1312 - external IDs list manager
1313
1314 Does NOT act on/listen to the current patient.
1315 """
1317
1318 wxgPersonIdentityManagerPnl.wxgPersonIdentityManagerPnl.__init__(self, *args, **kwargs)
1319
1320 self.__identity = None
1321 self.refresh()
1322 #--------------------------------------------------------
1323 # external API
1324 #--------------------------------------------------------
1326 self._PNL_names.identity = self.__identity
1327 self._PNL_ids.identity = self.__identity
1328 # this is an Edit Area:
1329 self._PNL_identity.mode = 'new'
1330 self._PNL_identity.data = self.__identity
1331 if self.__identity is not None:
1332 self._PNL_identity.mode = 'edit'
1333 self._PNL_identity._refresh_from_existing()
1334 #--------------------------------------------------------
1335 # properties
1336 #--------------------------------------------------------
1339
1343
1344 identity = property(_get_identity, _set_identity)
1345 #--------------------------------------------------------
1346 # event handlers
1347 #--------------------------------------------------------
1351 #self._PNL_identity.refresh()
1352 #--------------------------------------------------------
1355
1356 #============================================================
1357 from Gnumed.wxGladeWidgets import wxgPersonSocialNetworkManagerPnl
1358
1359 -class cPersonSocialNetworkManagerPnl(wxgPersonSocialNetworkManagerPnl.wxgPersonSocialNetworkManagerPnl):
1361
1362 wxgPersonSocialNetworkManagerPnl.wxgPersonSocialNetworkManagerPnl.__init__(self, *args, **kwargs)
1363
1364 self.__identity = None
1365 self._PRW_provider.selection_only = False
1366 self.refresh()
1367 #--------------------------------------------------------
1368 # external API
1369 #--------------------------------------------------------
1371
1372 tt = _('Link another person in this database as the emergency contact:\n\nEnter person name part or identifier and hit <enter>.')
1373
1374 if self.__identity is None:
1375 self._TCTRL_er_contact.SetValue(u'')
1376 self._TCTRL_person.person = None
1377 self._TCTRL_person.SetToolTipString(tt)
1378
1379 self._PRW_provider.SetText(value = u'', data = None)
1380 return
1381
1382 self._TCTRL_er_contact.SetValue(gmTools.coalesce(self.__identity['emergency_contact'], u''))
1383 if self.__identity['pk_emergency_contact'] is not None:
1384 ident = gmPerson.cIdentity(aPK_obj = self.__identity['pk_emergency_contact'])
1385 self._TCTRL_person.person = ident
1386 tt = u'%s\n\n%s\n\n%s' % (
1387 tt,
1388 ident['description_gender'],
1389 u'\n'.join([
1390 u'%s: %s%s' % (
1391 c['l10n_comm_type'],
1392 c['url'],
1393 gmTools.bool2subst(c['is_confidential'], _(' (confidential !)'), u'', u'')
1394 )
1395 for c in ident.get_comm_channels()
1396 ])
1397 )
1398 else:
1399 self._TCTRL_person.person = None
1400
1401 self._TCTRL_person.SetToolTipString(tt)
1402
1403 if self.__identity['pk_primary_provider'] is None:
1404 self._PRW_provider.SetText(value = u'', data = None)
1405 else:
1406 self._PRW_provider.SetData(data = self.__identity['pk_primary_provider'])
1407 #--------------------------------------------------------
1408 # properties
1409 #--------------------------------------------------------
1412
1416
1417 identity = property(_get_identity, _set_identity)
1418 #--------------------------------------------------------
1419 # event handlers
1420 #--------------------------------------------------------
1435 #--------------------------------------------------------
1438 #--------------------------------------------------------
1449 #--------------------------------------------------------
1457 #============================================================
1458 # new-patient widgets
1459 #============================================================
1461
1462 dbcfg = gmCfg.cCfgSQL()
1463
1464 def_region = dbcfg.get2 (
1465 option = u'person.create.default_region',
1466 workplace = gmSurgery.gmCurrentPractice().active_workplace,
1467 bias = u'user'
1468 )
1469 def_country = None
1470
1471 if def_region is None:
1472 def_country = dbcfg.get2 (
1473 option = u'person.create.default_country',
1474 workplace = gmSurgery.gmCurrentPractice().active_workplace,
1475 bias = u'user'
1476 )
1477 else:
1478 countries = gmDemographicRecord.get_country_for_region(region = def_region)
1479 if len(countries) == 1:
1480 def_country = countries[0]['code_country']
1481
1482 if parent is None:
1483 parent = wx.GetApp().GetTopWindow()
1484
1485 ea = cNewPatientEAPnl(parent = parent, id = -1, country = def_country, region = def_region)
1486 dlg = gmEditArea.cGenericEditAreaDlg2(parent = parent, id = -1, edit_area = ea, single_entry = True)
1487 dlg.SetTitle(_('Adding new person'))
1488 ea._PRW_lastname.SetFocus()
1489 result = dlg.ShowModal()
1490 pat = ea.data
1491 dlg.Destroy()
1492
1493 if result != wx.ID_OK:
1494 return False
1495
1496 _log.debug('created new person [%s]', pat.ID)
1497
1498 if activate:
1499 from Gnumed.wxpython import gmPatSearchWidgets
1500 gmPatSearchWidgets.set_active_patient(patient = pat)
1501
1502 gmDispatcher.send(signal = 'display_widget', name = 'gmNotebookedPatientEditionPlugin')
1503
1504 return True
1505 #============================================================
1506 from Gnumed.wxGladeWidgets import wxgNewPatientEAPnl
1507
1508 -class cNewPatientEAPnl(wxgNewPatientEAPnl.wxgNewPatientEAPnl, gmEditArea.cGenericEditAreaMixin):
1509
1511
1512 try:
1513 self.default_region = kwargs['region']
1514 del kwargs['region']
1515 except KeyError:
1516 self.default_region = None
1517
1518 try:
1519 self.default_country = kwargs['country']
1520 del kwargs['country']
1521 except KeyError:
1522 self.default_country = None
1523
1524 wxgNewPatientEAPnl.wxgNewPatientEAPnl.__init__(self, *args, **kwargs)
1525 gmEditArea.cGenericEditAreaMixin.__init__(self)
1526
1527 self.mode = 'new'
1528 self.data = None
1529 self._address = None
1530
1531 self.__init_ui()
1532 self.__register_interests()
1533 #----------------------------------------------------------------
1534 # internal helpers
1535 #----------------------------------------------------------------
1537 self._PRW_lastname.final_regex = '.+'
1538 self._PRW_firstnames.final_regex = '.+'
1539 self._PRW_address_searcher.selection_only = False
1540
1541 # only if we would support None on selection_only's:
1542 # self._PRW_external_id_type.selection_only = True
1543
1544 if self.default_country is not None:
1545 match = self._PRW_country._data2match(data = self.default_country)
1546 if match is not None:
1547 self._PRW_country.SetText(value = match['field_label'], data = match['data'])
1548
1549 if self.default_region is not None:
1550 self._PRW_region.SetText(value = self.default_region)
1551 #----------------------------------------------------------------
1553
1554 adr = self._PRW_address_searcher.address
1555 if adr is None:
1556 return True
1557
1558 if ctrl.GetValue().strip() != adr[field]:
1559 wx.CallAfter(self._PRW_address_searcher.SetText, value = u'', data = None)
1560 return True
1561
1562 return False
1563 #----------------------------------------------------------------
1565 adr = self._PRW_address_searcher.address
1566 if adr is None:
1567 return True
1568
1569 self._PRW_zip.SetText(value = adr['postcode'], data = adr['postcode'])
1570
1571 self._PRW_street.SetText(value = adr['street'], data = adr['street'])
1572 self._PRW_street.set_context(context = u'zip', val = adr['postcode'])
1573
1574 self._PRW_urb.SetText(value = adr['urb'], data = adr['urb'])
1575 self._PRW_urb.set_context(context = u'zip', val = adr['postcode'])
1576
1577 self._PRW_region.SetText(value = adr['l10n_state'], data = adr['code_state'])
1578 self._PRW_region.set_context(context = u'zip', val = adr['postcode'])
1579
1580 self._PRW_country.SetText(value = adr['l10n_country'], data = adr['code_country'])
1581 self._PRW_country.set_context(context = u'zip', val = adr['postcode'])
1582 #----------------------------------------------------------------
1584 error = False
1585
1586 # name fields
1587 if self._PRW_lastname.GetValue().strip() == u'':
1588 error = True
1589 gmDispatcher.send(signal = 'statustext', msg = _('Must enter lastname.'))
1590 self._PRW_lastname.display_as_valid(False)
1591 else:
1592 self._PRW_lastname.display_as_valid(True)
1593
1594 if self._PRW_firstnames.GetValue().strip() == '':
1595 error = True
1596 gmDispatcher.send(signal = 'statustext', msg = _('Must enter first name.'))
1597 self._PRW_firstnames.display_as_valid(False)
1598 else:
1599 self._PRW_firstnames.display_as_valid(True)
1600
1601 # gender
1602 if self._PRW_gender.GetData() is None:
1603 error = True
1604 gmDispatcher.send(signal = 'statustext', msg = _('Must select gender.'))
1605 self._PRW_gender.display_as_valid(False)
1606 else:
1607 self._PRW_gender.display_as_valid(True)
1608
1609 # dob validation
1610 if not _validate_dob_field(self._PRW_dob):
1611 error = True
1612
1613 # TOB validation if non-empty
1614 # if self._TCTRL_tob.GetValue().strip() != u'':
1615
1616 return (not error)
1617 #----------------------------------------------------------------
1619
1620 # existing address ? if so set other fields
1621 if self._PRW_address_searcher.GetData() is not None:
1622 wx.CallAfter(self.__set_fields_from_address_searcher)
1623 return True
1624
1625 # must either all contain something or none of them
1626 fields_to_fill = (
1627 self._TCTRL_number,
1628 self._PRW_zip,
1629 self._PRW_street,
1630 self._PRW_urb,
1631 self._PRW_type
1632 )
1633 no_of_filled_fields = 0
1634
1635 for field in fields_to_fill:
1636 if field.GetValue().strip() != u'':
1637 no_of_filled_fields += 1
1638 field.SetBackgroundColour(gmPhraseWheel.color_prw_valid)
1639 field.Refresh()
1640
1641 # empty address ?
1642 if no_of_filled_fields == 0:
1643 if empty_address_is_valid:
1644 return True
1645 else:
1646 return None
1647
1648 # incompletely filled address ?
1649 if no_of_filled_fields != len(fields_to_fill):
1650 for field in fields_to_fill:
1651 if field.GetValue().strip() == u'':
1652 field.SetBackgroundColour(gmPhraseWheel.color_prw_invalid)
1653 field.SetFocus()
1654 field.Refresh()
1655 msg = _('To properly create an address, all the related fields must be filled in.')
1656 gmGuiHelpers.gm_show_error(msg, _('Required fields'))
1657 return False
1658
1659 # fields which must contain a selected item
1660 # FIXME: they must also contain an *acceptable combination* which
1661 # FIXME: can only be tested against the database itself ...
1662 strict_fields = (
1663 self._PRW_type,
1664 self._PRW_region,
1665 self._PRW_country
1666 )
1667 error = False
1668 for field in strict_fields:
1669 if field.GetData() is None:
1670 error = True
1671 field.SetBackgroundColour(gmPhraseWheel.color_prw_invalid)
1672 field.SetFocus()
1673 else:
1674 field.SetBackgroundColour(gmPhraseWheel.color_prw_valid)
1675 field.Refresh()
1676
1677 if error:
1678 msg = _('This field must contain an item selected from the dropdown list.')
1679 gmGuiHelpers.gm_show_error(msg, _('Required fields'))
1680 return False
1681
1682 return True
1683 #----------------------------------------------------------------
1685
1686 # identity
1687 self._PRW_firstnames.add_callback_on_lose_focus(self._on_leaving_firstname)
1688
1689 # address
1690 self._PRW_address_searcher.add_callback_on_lose_focus(self._on_leaving_adress_searcher)
1691
1692 # invalidate address searcher when any field edited
1693 self._PRW_street.add_callback_on_lose_focus(self._invalidate_address_searcher)
1694 wx.EVT_KILL_FOCUS(self._TCTRL_number, self._on_leaving_number)
1695 wx.EVT_KILL_FOCUS(self._TCTRL_unit, self._on_leaving_unit)
1696 self._PRW_urb.add_callback_on_lose_focus(self._invalidate_address_searcher)
1697 self._PRW_region.add_callback_on_lose_focus(self._invalidate_address_searcher)
1698
1699 self._PRW_zip.add_callback_on_lose_focus(self._on_leaving_zip)
1700 self._PRW_country.add_callback_on_lose_focus(self._on_leaving_country)
1701 #----------------------------------------------------------------
1702 # event handlers
1703 #----------------------------------------------------------------
1705 """Set the gender according to entered firstname.
1706
1707 Matches are fetched from existing records in backend.
1708 """
1709 # only set if not already set so as to not
1710 # overwrite a change by the user
1711 if self._PRW_gender.GetData() is not None:
1712 return True
1713
1714 firstname = self._PRW_firstnames.GetValue().strip()
1715 if firstname == u'':
1716 return True
1717
1718 gender = gmPerson.map_firstnames2gender(firstnames = firstname)
1719 if gender is None:
1720 return True
1721
1722 wx.CallAfter(self._PRW_gender.SetData, gender)
1723 return True
1724 #----------------------------------------------------------------
1726 self.__perhaps_invalidate_address_searcher(self._PRW_zip, 'postcode')
1727
1728 zip_code = gmTools.none_if(self._PRW_zip.GetValue().strip(), u'')
1729 self._PRW_street.set_context(context = u'zip', val = zip_code)
1730 self._PRW_urb.set_context(context = u'zip', val = zip_code)
1731 self._PRW_region.set_context(context = u'zip', val = zip_code)
1732 self._PRW_country.set_context(context = u'zip', val = zip_code)
1733
1734 return True
1735 #----------------------------------------------------------------
1737 self.__perhaps_invalidate_address_searcher(self._PRW_country, 'l10n_country')
1738
1739 country = gmTools.none_if(self._PRW_country.GetValue().strip(), u'')
1740 self._PRW_region.set_context(context = u'country', val = country)
1741
1742 return True
1743 #----------------------------------------------------------------
1745 if self._TCTRL_number.GetValue().strip() == u'':
1746 adr = self._PRW_address_searcher.address
1747 if adr is None:
1748 return True
1749 self._TCTRL_number.SetValue(adr['number'])
1750 return True
1751
1752 self.__perhaps_invalidate_address_searcher(self._TCTRL_number, 'number')
1753 return True
1754 #----------------------------------------------------------------
1756 if self._TCTRL_unit.GetValue().strip() == u'':
1757 adr = self._PRW_address_searcher.address
1758 if adr is None:
1759 return True
1760 self._TCTRL_unit.SetValue(gmTools.coalesce(adr['subunit'], u''))
1761 return True
1762
1763 self.__perhaps_invalidate_address_searcher(self._TCTRL_unit, 'subunit')
1764 return True
1765 #----------------------------------------------------------------
1767 mapping = [
1768 (self._PRW_street, 'street'),
1769 (self._PRW_urb, 'urb'),
1770 (self._PRW_region, 'l10n_state')
1771 ]
1772 # loop through fields and invalidate address searcher if different
1773 for ctrl, field in mapping:
1774 if self.__perhaps_invalidate_address_searcher(ctrl, field):
1775 return True
1776
1777 return True
1778 #----------------------------------------------------------------
1780 if self._PRW_address_searcher.address is None:
1781 return True
1782
1783 wx.CallAfter(self.__set_fields_from_address_searcher)
1784 return True
1785 #----------------------------------------------------------------
1786 # generic Edit Area mixin API
1787 #----------------------------------------------------------------
1789 if self._PRW_primary_provider.GetValue().strip() == u'':
1790 self._PRW_primary_provider.display_as_valid(True)
1791 else:
1792 if self._PRW_primary_provider.GetData() is None:
1793 self._PRW_primary_provider.display_as_valid(False)
1794 else:
1795 self._PRW_primary_provider.display_as_valid(True)
1796 return (self.__identity_valid_for_save() and self.__address_valid_for_save(empty_address_is_valid = True))
1797 #----------------------------------------------------------------
1799
1800 if self._PRW_dob.GetValue().strip() == u'':
1801 if not _empty_dob_allowed():
1802 self._PRW_dob.display_as_valid(False)
1803 self._PRW_dob.SetFocus()
1804 return False
1805
1806 # identity
1807 new_identity = gmPerson.create_identity (
1808 gender = self._PRW_gender.GetData(),
1809 dob = self._PRW_dob.GetData(),
1810 lastnames = self._PRW_lastname.GetValue().strip(),
1811 firstnames = self._PRW_firstnames.GetValue().strip()
1812 )
1813 _log.debug('identity created: %s' % new_identity)
1814
1815 new_identity['title'] = gmTools.none_if(self._PRW_title.GetValue().strip())
1816 new_identity.set_nickname(nickname = gmTools.none_if(self._PRW_nickname.GetValue().strip(), u''))
1817 #TOB
1818 prov = self._PRW_primary_provider.GetData()
1819 if prov is not None:
1820 new_identity['pk_primary_provider'] = prov
1821 new_identity['comment'] = gmTools.none_if(self._TCTRL_comment.GetValue().strip(), u'')
1822 new_identity.save()
1823
1824 # address
1825 # if we reach this the address cannot be completely empty
1826 is_valid = self.__address_valid_for_save(empty_address_is_valid = False)
1827 if is_valid is True:
1828 # because we currently only check for non-emptiness
1829 # we must still deal with database errors
1830 try:
1831 new_identity.link_address (
1832 number = self._TCTRL_number.GetValue().strip(),
1833 street = self._PRW_street.GetValue().strip(),
1834 postcode = self._PRW_zip.GetValue().strip(),
1835 urb = self._PRW_urb.GetValue().strip(),
1836 state = self._PRW_region.GetData(),
1837 country = self._PRW_country.GetData(),
1838 subunit = gmTools.none_if(self._TCTRL_unit.GetValue().strip(), u''),
1839 id_type = self._PRW_type.GetData()
1840 )
1841 except gmPG2.dbapi.InternalError:
1842 _log.debug('number: >>%s<<', self._TCTRL_number.GetValue().strip())
1843 _log.debug('(sub)unit: >>%s<<', self._TCTRL_unit.GetValue().strip())
1844 _log.debug('street: >>%s<<', self._PRW_street.GetValue().strip())
1845 _log.debug('postcode: >>%s<<', self._PRW_zip.GetValue().strip())
1846 _log.debug('urb: >>%s<<', self._PRW_urb.GetValue().strip())
1847 _log.debug('state: >>%s<<', self._PRW_region.GetData().strip())
1848 _log.debug('country: >>%s<<', self._PRW_country.GetData().strip())
1849 _log.exception('cannot link address')
1850 gmGuiHelpers.gm_show_error (
1851 aTitle = _('Saving address'),
1852 aMessage = _(
1853 'Cannot save this address.\n'
1854 '\n'
1855 'You will have to add it via the Demographics plugin.\n'
1856 )
1857 )
1858 elif is_valid is False:
1859 gmGuiHelpers.gm_show_error (
1860 aTitle = _('Saving address'),
1861 aMessage = _(
1862 'Address not saved.\n'
1863 '\n'
1864 'You will have to add it via the Demographics plugin.\n'
1865 )
1866 )
1867 # else it is None which means empty address which we ignore
1868
1869 # phone
1870 channel_name = self._PRW_channel_type.GetValue().strip()
1871 pk_channel_type = self._PRW_channel_type.GetData()
1872 if pk_channel_type is None:
1873 if channel_name == u'':
1874 channel_name = u'homephone'
1875 new_identity.link_comm_channel (
1876 comm_medium = channel_name,
1877 pk_channel_type = pk_channel_type,
1878 url = gmTools.none_if(self._TCTRL_phone.GetValue().strip(), u''),
1879 is_confidential = False
1880 )
1881
1882 # external ID
1883 pk_type = self._PRW_external_id_type.GetData()
1884 id_value = self._TCTRL_external_id_value.GetValue().strip()
1885 if (pk_type is not None) and (id_value != u''):
1886 new_identity.add_external_id(value = id_value, pk_type = pk_type)
1887
1888 # occupation
1889 new_identity.link_occupation (
1890 occupation = gmTools.none_if(self._PRW_occupation.GetValue().strip(), u'')
1891 )
1892
1893 self.data = new_identity
1894 return True
1895 #----------------------------------------------------------------
1898 #----------------------------------------------------------------
1902 #----------------------------------------------------------------
1905 #----------------------------------------------------------------
1908
1909 #============================================================
1910 # patient demographics editing classes
1911 #============================================================
1913 """Notebook displaying demographics editing pages:
1914
1915 - Identity (as per Jim/Rogerio 12/2011)
1916 - Contacts (addresses, phone numbers, etc)
1917 - Social network (significant others, GP, etc)
1918
1919 Does NOT act on/listen to the current patient.
1920 """
1921 #--------------------------------------------------------
1923
1924 wx.Notebook.__init__ (
1925 self,
1926 parent = parent,
1927 id = id,
1928 style = wx.NB_TOP | wx.NB_MULTILINE | wx.NO_BORDER,
1929 name = self.__class__.__name__
1930 )
1931
1932 self.__identity = None
1933 self.__do_layout()
1934 self.SetSelection(0)
1935 #--------------------------------------------------------
1936 # public API
1937 #--------------------------------------------------------
1939 """Populate fields in pages with data from model."""
1940 for page_idx in range(self.GetPageCount()):
1941 page = self.GetPage(page_idx)
1942 page.identity = self.__identity
1943
1944 return True
1945 #--------------------------------------------------------
1946 # internal API
1947 #--------------------------------------------------------
1949 """Build patient edition notebook pages."""
1950
1951 # identity page
1952 new_page = cPersonIdentityManagerPnl(self, -1)
1953 new_page.identity = self.__identity
1954 self.AddPage (
1955 page = new_page,
1956 text = _('Identity'),
1957 select = False
1958 )
1959
1960 # contacts page
1961 new_page = gmPersonContactWidgets.cPersonContactsManagerPnl(self, -1)
1962 new_page.identity = self.__identity
1963 self.AddPage (
1964 page = new_page,
1965 text = _('Contacts'),
1966 select = True
1967 )
1968
1969 # social network page
1970 new_page = cPersonSocialNetworkManagerPnl(self, -1)
1971 new_page.identity = self.__identity
1972 self.AddPage (
1973 page = new_page,
1974 text = _('Social network'),
1975 select = False
1976 )
1977 #--------------------------------------------------------
1978 # properties
1979 #--------------------------------------------------------
1982
1984 self.__identity = identity
1985
1986 identity = property(_get_identity, _set_identity)
1987 #============================================================
1988 # old occupation widgets
1989 #============================================================
1990 # FIXME: support multiple occupations
1991 # FIXME: redo with wxGlade
1992
1994 """Page containing patient occupations edition fields.
1995 """
1997 """
1998 Creates a new instance of BasicPatDetailsPage
1999 @param parent - The parent widget
2000 @type parent - A wx.Window instance
2001 @param id - The widget id
2002 @type id - An integer
2003 """
2004 wx.Panel.__init__(self, parent, id)
2005 self.__ident = ident
2006 self.__do_layout()
2007 #--------------------------------------------------------
2009 PNL_form = wx.Panel(self, -1)
2010 # occupation
2011 STT_occupation = wx.StaticText(PNL_form, -1, _('Occupation'))
2012 self.PRW_occupation = cOccupationPhraseWheel(parent = PNL_form, id = -1)
2013 self.PRW_occupation.SetToolTipString(_("primary occupation of the patient"))
2014 # known since
2015 STT_occupation_updated = wx.StaticText(PNL_form, -1, _('Last updated'))
2016 self.TTC_occupation_updated = wx.TextCtrl(PNL_form, -1, style = wx.TE_READONLY)
2017
2018 # layout input widgets
2019 SZR_input = wx.FlexGridSizer(cols = 2, rows = 5, vgap = 4, hgap = 4)
2020 SZR_input.AddGrowableCol(1)
2021 SZR_input.Add(STT_occupation, 0, wx.SHAPED)
2022 SZR_input.Add(self.PRW_occupation, 1, wx.EXPAND)
2023 SZR_input.Add(STT_occupation_updated, 0, wx.SHAPED)
2024 SZR_input.Add(self.TTC_occupation_updated, 1, wx.EXPAND)
2025 PNL_form.SetSizerAndFit(SZR_input)
2026
2027 # layout page
2028 SZR_main = wx.BoxSizer(wx.VERTICAL)
2029 SZR_main.Add(PNL_form, 1, wx.EXPAND)
2030 self.SetSizer(SZR_main)
2031 #--------------------------------------------------------
2034 #--------------------------------------------------------
2036 if identity is not None:
2037 self.__ident = identity
2038 jobs = self.__ident.get_occupations()
2039 if len(jobs) > 0:
2040 self.PRW_occupation.SetText(jobs[0]['l10n_occupation'])
2041 self.TTC_occupation_updated.SetValue(jobs[0]['modified_when'].strftime('%m/%Y'))
2042 return True
2043 #--------------------------------------------------------
2045 if self.PRW_occupation.IsModified():
2046 new_job = self.PRW_occupation.GetValue().strip()
2047 jobs = self.__ident.get_occupations()
2048 for job in jobs:
2049 if job['l10n_occupation'] == new_job:
2050 continue
2051 self.__ident.unlink_occupation(occupation = job['l10n_occupation'])
2052 self.__ident.link_occupation(occupation = new_job)
2053 return True
2054 #============================================================
2056 """Patient demographics plugin for main notebook.
2057
2058 Hosts another notebook with pages for Identity, Contacts, etc.
2059
2060 Acts on/listens to the currently active patient.
2061 """
2062 #--------------------------------------------------------
2064 wx.Panel.__init__ (self, parent = parent, id = id, style = wx.NO_BORDER)
2065 gmRegetMixin.cRegetOnPaintMixin.__init__(self)
2066 self.__do_layout()
2067 self.__register_interests()
2068 #--------------------------------------------------------
2069 # public API
2070 #--------------------------------------------------------
2071 #--------------------------------------------------------
2072 # internal helpers
2073 #--------------------------------------------------------
2075 """Arrange widgets."""
2076 self.__patient_notebook = cPersonDemographicsEditorNb(self, -1)
2077
2078 szr_main = wx.BoxSizer(wx.VERTICAL)
2079 szr_main.Add(self.__patient_notebook, 1, wx.EXPAND)
2080 self.SetSizerAndFit(szr_main)
2081 #--------------------------------------------------------
2082 # event handling
2083 #--------------------------------------------------------
2085 gmDispatcher.connect(signal = u'pre_patient_selection', receiver = self._on_pre_patient_selection)
2086 gmDispatcher.connect(signal = u'post_patient_selection', receiver = self._on_post_patient_selection)
2087 #--------------------------------------------------------
2090 #--------------------------------------------------------
2093 # reget mixin API
2094 #--------------------------------------------------------
2104 #============================================================
2105 #============================================================
2106 if __name__ == "__main__":
2107
2108 #--------------------------------------------------------
2110 app = wx.PyWidgetTester(size = (600, 400))
2111 app.SetWidget(cKOrganizerSchedulePnl)
2112 app.MainLoop()
2113 #--------------------------------------------------------
2115 app = wx.PyWidgetTester(size = (600, 400))
2116 widget = cPersonNamesManagerPnl(app.frame, -1)
2117 widget.identity = activate_patient()
2118 app.frame.Show(True)
2119 app.MainLoop()
2120 #--------------------------------------------------------
2122 app = wx.PyWidgetTester(size = (600, 400))
2123 widget = cPersonIDsManagerPnl(app.frame, -1)
2124 widget.identity = activate_patient()
2125 app.frame.Show(True)
2126 app.MainLoop()
2127 #--------------------------------------------------------
2129 app = wx.PyWidgetTester(size = (600, 400))
2130 widget = cPersonIdentityManagerPnl(app.frame, -1)
2131 widget.identity = activate_patient()
2132 app.frame.Show(True)
2133 app.MainLoop()
2134 #--------------------------------------------------------
2136 app = wx.PyWidgetTester(size = (600, 400))
2137 app.SetWidget(cPersonNameEAPnl, name = activate_patient().get_active_name())
2138 app.MainLoop()
2139 #--------------------------------------------------------
2141 app = wx.PyWidgetTester(size = (600, 400))
2142 widget = cPersonDemographicsEditorNb(app.frame, -1)
2143 widget.identity = activate_patient()
2144 widget.refresh()
2145 app.frame.Show(True)
2146 app.MainLoop()
2147 #--------------------------------------------------------
2149 patient = gmPersonSearch.ask_for_patient()
2150 if patient is None:
2151 print "No patient. Exiting gracefully..."
2152 sys.exit(0)
2153 from Gnumed.wxpython import gmPatSearchWidgets
2154 gmPatSearchWidgets.set_active_patient(patient=patient)
2155 return patient
2156 #--------------------------------------------------------
2157 if len(sys.argv) > 1 and sys.argv[1] == 'test':
2158
2159 gmI18N.activate_locale()
2160 gmI18N.install_domain(domain='gnumed')
2161 gmPG2.get_connection()
2162
2163 # app = wx.PyWidgetTester(size = (400, 300))
2164 # app.SetWidget(cNotebookedPatEditionPanel, -1)
2165 # app.frame.Show(True)
2166 # app.MainLoop()
2167
2168 # phrasewheels
2169 # test_organizer_pnl()
2170
2171 # identity related widgets
2172 #test_person_names_pnl()
2173 test_person_ids_pnl()
2174 #test_pat_ids_pnl()
2175 #test_name_ea_pnl()
2176
2177 #test_cPersonDemographicsEditorNb()
2178
2179 #============================================================
2180
| Home | Trees | Indices | Help |
|
|---|
| Generated by Epydoc 3.0.1 on Mon Dec 5 03:59:57 2011 | http://epydoc.sourceforge.net |