| Home | Trees | Indices | Help |
|
|---|
|
|
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 # standard library
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 # GNUmed specific
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 # constant defs
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 # image tags related widgets
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
149
151
152 try:
153 data = kwargs['tag_image']
154 del kwargs['tag_image']
155 except KeyError:
156 data = None
157
158 wxgTagImageEAPnl.wxgTagImageEAPnl.__init__(self, *args, **kwargs)
159 gmEditArea.cGenericEditAreaMixin.__init__(self)
160
161 self.mode = 'new'
162 self.data = data
163 if data is not None:
164 self.mode = 'edit'
165
166 self.__selected_image_file = None
167 #----------------------------------------------------------------
168 # generic Edit Area mixin API
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 #----------------------------------------------------------------
199
200 dbo_conn = gmAuthWidgets.get_dbowner_connection(procedure = _('Creating tag with image'))
201 if dbo_conn is None:
202 return False
203
204 data = gmDemographicRecord.create_tag_image(description = self._TCTRL_description.GetValue().strip(), link_obj = dbo_conn)
205 dbo_conn.close()
206
207 data['filename'] = self._TCTRL_filename.GetValue().strip()
208 data.save()
209 data.update_image_from_file(filename = self.__selected_image_file)
210
211 # must be done very late or else the property access
212 # will refresh the display such that later field
213 # access will return empty values
214 self.data = data
215 return True
216 #----------------------------------------------------------------
218
219 # this is somewhat fake as it never actually uses the gm-dbo conn
220 # (although it does verify it)
221 dbo_conn = gmAuthWidgets.get_dbowner_connection(procedure = _('Updating tag with image'))
222 if dbo_conn is None:
223 return False
224 dbo_conn.close()
225
226 self.data['description'] = self._TCTRL_description.GetValue().strip()
227 self.data['filename'] = self._TCTRL_filename.GetValue().strip()
228 self.data.save()
229
230 if self.__selected_image_file is not None:
231 open(self.__selected_image_file).close()
232 self.data.update_image_from_file(filename = self.__selected_image_file)
233 self.__selected_image_file = None
234
235 return True
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 #----------------------------------------------------------------
248 #----------------------------------------------------------------
250 self._TCTRL_description.SetValue(self.data['l10n_description'])
251 self._TCTRL_filename.SetValue(gmTools.coalesce(self.data['filename'], u''))
252 fname = self.data.export_image2file()
253 if fname is None:
254 self._BMP_image.SetBitmap(bitmap = wx.EmptyBitmap(100, 100))
255 else:
256 self._BMP_image.SetBitmap(bitmap = gmGuiHelpers.file2scaled_image(filename = fname, height = 100))
257
258 self.__selected_image_file = None
259
260 self._TCTRL_description.SetFocus()
261 #----------------------------------------------------------------
262 # event handlers
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 # external API
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 # FIXME: add context menu for Delete/Clone/Add/Configure
314 self._SZR_bitmaps.Add(bmp, 0, wx.LEFT | wx.RIGHT | wx.TOP | wx.BOTTOM, 1) # | wx.EXPAND
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 # internal helpers
327 #--------------------------------------------------------
329 if self.__current_tag is None:
330 return
331 pat = gmPerson.gmCurrentPatient()
332 if not pat.connected:
333 return
334 pat.remove_tag(tag = self.__current_tag['pk_identity_tag'])
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 # event handlers
362 #--------------------------------------------------------
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 #--------------------------------------------------------
392 """Reload appointments from KOrganizer."""
393 found, cmd = gmShellAPI.detect_external_binary(binary = 'korganizer')
394
395 if not found:
396 gmDispatcher.send(signal = 'statustext', msg = _('KOrganizer is not installed.'), beep = True)
397 return
398
399 gmShellAPI.run_command_in_shell(command = cmd, blocking = False)
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 # start_date, start_time, end_date, end_time, title (patient), ort, comment, UID
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 # notebook plugins API
436 #--------------------------------------------------------
438 self.reload_appointments()
439 #============================================================
440 # occupation related widgets / functions
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 # unlink all but the new job
472 if job['l10n_occupation'] != new_job:
473 pat.unlink_occupation(occupation = job['l10n_occupation'])
474 # and link the new one
475 pat.link_occupation(occupation = new_job)
476
477 #------------------------------------------------------------
479
481 query = u"SELECT distinct name, _(name) from dem.occupation where _(name) %(fragment_condition)s"
482 mp = gmMatchProvider.cMatchProvider_SQL2(queries=query)
483 mp.setThresholds(1, 3, 5)
484 gmPhraseWheel.cPhraseWheel.__init__ (
485 self,
486 *args,
487 **kwargs
488 )
489 self.SetToolTipString(_("Type or select an occupation."))
490 self.capitalisation_mode = gmTools.CAPS_FIRST
491 self.matcher = mp
492
493 #============================================================
494 # identity widgets / functions
495 #============================================================
497 # ask user for assurance
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 # get admin connection
523 conn = gmAuthWidgets.get_dbowner_connection (
524 procedure = _('Disabling patient')
525 )
526 # - user cancelled
527 if conn is False:
528 return True
529 # - error
530 if conn is None:
531 return False
532
533 # now disable patient
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 # phrasewheels
540 #------------------------------------------------------------
542
544 query = u"SELECT distinct lastnames, lastnames from dem.names where lastnames %(fragment_condition)s order by lastnames limit 25"
545 mp = gmMatchProvider.cMatchProvider_SQL2(queries=query)
546 mp.setThresholds(3, 5, 9)
547 gmPhraseWheel.cPhraseWheel.__init__ (
548 self,
549 *args,
550 **kwargs
551 )
552 self.SetToolTipString(_("Type or select a last name (family name/surname)."))
553 self.capitalisation_mode = gmTools.CAPS_NAMES
554 self.matcher = mp
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 # nicknames CAN start with lower case !
592 #self.capitalisation_mode = gmTools.CAPS_NAMES
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 #------------------------------------------------------------
664
666 query = u"""
667 SELECT distinct issuer, issuer
668 from dem.enum_ext_id_types
669 where issuer %(fragment_condition)s
670 order by issuer limit 25"""
671 mp = gmMatchProvider.cMatchProvider_SQL2(queries=query)
672 mp.setThresholds(1, 3, 5)
673 gmPhraseWheel.cPhraseWheel.__init__(self, *args, **kwargs)
674 self.SetToolTipString(_("Type or select an ID issuer."))
675 self.capitalisation_mode = gmTools.CAPS_FIRST
676 self.matcher = mp
677 #------------------------------------------------------------
678 # edit areas
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 """
688
689 try:
690 data = kwargs['external_id']
691 del kwargs['external_id']
692 except:
693 data = None
694
695 wxgExternalIDEditAreaPnl.wxgExternalIDEditAreaPnl.__init__(self, *args, **kwargs)
696 gmEditArea.cGenericEditAreaMixin.__init__(self)
697
698 self.identity = None
699
700 self.mode = 'new'
701 self.data = data
702 if data is not None:
703 self.mode = 'edit'
704
705 self.__init_ui()
706 #--------------------------------------------------------
708 self._PRW_type.add_callback_on_lose_focus(self._on_type_set)
709 #----------------------------------------------------------------
710 # generic Edit Area mixin API
711 #----------------------------------------------------------------
713 validity = True
714
715 # do not test .GetData() because adding external
716 # IDs will create types as necessary
717 #if self._PRW_type.GetData() is None:
718 if self._PRW_type.GetValue().strip() == u'':
719 validity = False
720 self._PRW_type.display_as_valid(False)
721 self._PRW_type.SetFocus()
722 else:
723 self._PRW_type.display_as_valid(True)
724
725 if self._TCTRL_value.GetValue().strip() == u'':
726 validity = False
727 self.display_tctrl_as_valid(tctrl = self._TCTRL_value, valid = False)
728 else:
729 self.display_tctrl_as_valid(tctrl = self._TCTRL_value, valid = True)
730
731 return validity
732 #----------------------------------------------------------------
734 data = {}
735 data['pk_type'] = None
736 data['name'] = self._PRW_type.GetValue().strip()
737 data['value'] = self._TCTRL_value.GetValue().strip()
738 data['issuer'] = gmTools.none_if(self._PRW_issuer.GetValue().strip(), u'')
739 data['comment'] = gmTools.none_if(self._TCTRL_comment.GetValue().strip(), u'')
740
741 self.identity.add_external_id (
742 type_name = data['name'],
743 value = data['value'],
744 issuer = data['issuer'],
745 comment = data['comment']
746 )
747
748 self.data = data
749 return True
750 #----------------------------------------------------------------
752 self.data['name'] = self._PRW_type.GetValue().strip()
753 self.data['value'] = self._TCTRL_value.GetValue().strip()
754 self.data['issuer'] = gmTools.none_if(self._PRW_issuer.GetValue().strip(), u'')
755 self.data['comment'] = gmTools.none_if(self._TCTRL_comment.GetValue().strip(), u'')
756
757 self.identity.update_external_id (
758 pk_id = self.data['pk_id'],
759 type = self.data['name'],
760 value = self.data['value'],
761 issuer = self.data['issuer'],
762 comment = self.data['comment']
763 )
764
765 return True
766 #----------------------------------------------------------------
768 self._PRW_type.SetText(value = u'', data = None)
769 self._TCTRL_value.SetValue(u'')
770 self._PRW_issuer.SetText(value = u'', data = None)
771 self._TCTRL_comment.SetValue(u'')
772 #----------------------------------------------------------------
776 #----------------------------------------------------------------
778 self._PRW_type.SetText(value = self.data['name'], data = self.data['pk_type'])
779 self._TCTRL_value.SetValue(self.data['value'])
780 self._PRW_issuer.SetText(self.data['issuer'])
781 self._TCTRL_comment.SetValue(gmTools.coalesce(self.data['comment'], u''))
782 #----------------------------------------------------------------
783 # internal helpers
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 # identity widgets
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 # valid timestamp ?
822 if dob_prw.is_valid_timestamp(allow_empty = False): # properly colors the field
823 dob = dob_prw.date
824 # but year also usable ?
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 # invalid timestamp but not empty
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 # empty DOB field
862 dob_prw.display_as_valid(False)
863 return True
864 #------------------------------------------------------------
865 from Gnumed.wxGladeWidgets import wxgIdentityEAPnl
866
868 """An edit area for editing/creating title/gender/dob/dod etc."""
869
871
872 try:
873 data = kwargs['identity']
874 del kwargs['identity']
875 except KeyError:
876 data = None
877
878 wxgIdentityEAPnl.wxgIdentityEAPnl.__init__(self, *args, **kwargs)
879 gmEditArea.cGenericEditAreaMixin.__init__(self)
880
881 self.mode = 'new'
882 self.data = data
883 if data is not None:
884 self.mode = 'edit'
885
886 # self.__init_ui()
887 #----------------------------------------------------------------
888 # def __init_ui(self):
889 # # adjust phrasewheels etc
890 #----------------------------------------------------------------
891 # generic Edit Area mixin API
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 #----------------------------------------------------------------
917
918 if self._PRW_dob.GetValue().strip() == u'':
919 if not _empty_dob_allowed():
920 return False
921 self.data['dob'] = None
922 else:
923 self.data['dob'] = self._PRW_dob.GetData()
924
925 self.data['gender'] = self._PRW_gender.GetData()
926 self.data['title'] = gmTools.none_if(self._PRW_title.GetValue().strip(), u'')
927 self.data['deceased'] = self._PRW_dod.GetData()
928 self.data['comment'] = gmTools.none_if(self._TCTRL_comment.GetValue().strip(), u'')
929
930 self.data.save()
931 return True
932 #----------------------------------------------------------------
935 #----------------------------------------------------------------
937
938 self._LBL_info.SetLabel(u'ID: #%s' % (
939 self.data.ID
940 # FIXME: add 'deleted' status
941 ))
942 if self.data['dob'] is None:
943 val = u''
944 else:
945 val = gmDateTime.pydt_strftime (
946 self.data['dob'],
947 format = '%Y-%m-%d %H:%M',
948 accuracy = gmDateTime.acc_minutes
949 )
950 self._PRW_dob.SetText(value = val, data = self.data['dob'])
951 if self.data['deceased'] is None:
952 val = u''
953 else:
954 val = gmDateTime.pydt_strftime (
955 self.data['deceased'],
956 format = '%Y-%m-%d %H:%M',
957 accuracy = gmDateTime.acc_minutes
958 )
959 self._PRW_dod.SetText(value = val, data = self.data['deceased'])
960 self._PRW_gender.SetData(self.data['gender'])
961 #self._PRW_ethnicity.SetValue()
962 self._PRW_title.SetText(gmTools.coalesce(self.data['title'], u''))
963 self._TCTRL_comment.SetValue(gmTools.coalesce(self.data['comment'], u''))
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 """
976
977 try:
978 data = kwargs['name']
979 identity = gmPerson.cIdentity(aPK_obj = data['pk_identity'])
980 del kwargs['name']
981 except KeyError:
982 data = None
983 identity = kwargs['identity']
984 del kwargs['identity']
985
986 wxgPersonNameEAPnl.wxgPersonNameEAPnl.__init__(self, *args, **kwargs)
987 gmEditArea.cGenericEditAreaMixin.__init__(self)
988
989 self.__identity = identity
990
991 self.mode = 'new'
992 self.data = data
993 if data is not None:
994 self.mode = 'edit'
995
996 #self.__init_ui()
997 #----------------------------------------------------------------
998 # def __init_ui(self):
999 # # adjust phrasewheels etc
1000 #----------------------------------------------------------------
1001 # generic Edit Area mixin API
1002 #----------------------------------------------------------------
1004 validity = True
1005
1006 if self._PRW_lastname.GetValue().strip() == u'':
1007 validity = False
1008 self._PRW_lastname.display_as_valid(False)
1009 self._PRW_lastname.SetFocus()
1010 else:
1011 self._PRW_lastname.display_as_valid(True)
1012
1013 if self._PRW_firstname.GetValue().strip() == u'':
1014 validity = False
1015 self._PRW_firstname.display_as_valid(False)
1016 self._PRW_firstname.SetFocus()
1017 else:
1018 self._PRW_firstname.display_as_valid(True)
1019
1020 return validity
1021 #----------------------------------------------------------------
1023
1024 first = self._PRW_firstname.GetValue().strip()
1025 last = self._PRW_lastname.GetValue().strip()
1026 active = self._CHBOX_active.GetValue()
1027
1028 data = self.__identity.add_name(first, last, active)
1029
1030 old_nick = self.__identity['active_name']['preferred']
1031 new_nick = gmTools.none_if(self._PRW_nick.GetValue().strip(), u'')
1032 if active:
1033 data['preferred'] = gmTools.coalesce(new_nick, old_nick)
1034 else:
1035 data['preferred'] = new_nick
1036 data['comment'] = gmTools.none_if(self._TCTRL_comment.GetValue().strip(), u'')
1037 data.save()
1038
1039 self.data = data
1040 return True
1041 #----------------------------------------------------------------
1043 """The knack here is that we can only update a few fields.
1044
1045 Otherwise we need to clone the name and update that.
1046 """
1047 first = self._PRW_firstname.GetValue().strip()
1048 last = self._PRW_lastname.GetValue().strip()
1049 active = self._CHBOX_active.GetValue()
1050
1051 current_name = self.data['firstnames'].strip() + self.data['lastnames'].strip()
1052 new_name = first + last
1053
1054 # editable fields only ?
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 # else clone name and update that
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 #----------------------------------------------------------------
1071 self._PRW_firstname.SetText(value = u'', data = None)
1072 self._PRW_lastname.SetText(value = u'', data = None)
1073 self._PRW_nick.SetText(value = u'', data = None)
1074 self._TCTRL_comment.SetValue(u'')
1075 self._CHBOX_active.SetValue(False)
1076
1077 self._PRW_firstname.SetFocus()
1078 #----------------------------------------------------------------
1080 self._refresh_as_new()
1081 self._PRW_firstname.SetText(value = u'', data = None)
1082 self._PRW_nick.SetText(gmTools.coalesce(self.data['preferred'], u''))
1083
1084 self._PRW_lastname.SetFocus()
1085 #----------------------------------------------------------------
1087 self._PRW_firstname.SetText(self.data['firstnames'])
1088 self._PRW_lastname.SetText(self.data['lastnames'])
1089 self._PRW_nick.SetText(gmTools.coalesce(self.data['preferred'], u''))
1090 self._TCTRL_comment.SetValue(gmTools.coalesce(self.data['comment'], u''))
1091 self._CHBOX_active.SetValue(self.data['active_name'])
1092
1093 self._TCTRL_comment.SetFocus()
1094 #------------------------------------------------------------
1095 # list manager
1096 #------------------------------------------------------------
1098 """A list for managing a person's names.
1099
1100 Does NOT act on/listen to the current patient.
1101 """
1103
1104 try:
1105 self.__identity = kwargs['identity']
1106 del kwargs['identity']
1107 except KeyError:
1108 self.__identity = None
1109
1110 gmListWidgets.cGenericListManagerPnl.__init__(self, *args, **kwargs)
1111
1112 self.new_callback = self._add_name
1113 self.edit_callback = self._edit_name
1114 self.delete_callback = self._del_name
1115 self.refresh_callback = self.refresh
1116
1117 self.__init_ui()
1118 self.refresh()
1119 #--------------------------------------------------------
1120 # external API
1121 #--------------------------------------------------------
1123 if self.__identity is None:
1124 self._LCTRL_items.set_string_items()
1125 return
1126
1127 names = self.__identity.get_names()
1128 self._LCTRL_items.set_string_items (
1129 items = [ [
1130 gmTools.bool2str(n['active_name'], 'X', ''),
1131 n['lastnames'],
1132 n['firstnames'],
1133 gmTools.coalesce(n['preferred'], u''),
1134 gmTools.coalesce(n['comment'], u'')
1135 ] for n in names ]
1136 )
1137 self._LCTRL_items.set_column_widths()
1138 self._LCTRL_items.set_data(data = names)
1139 #--------------------------------------------------------
1140 # internal helpers
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 #--------------------------------------------------------
1153 #ea = cPersonNameEAPnl(self, -1, name = self.__identity.get_active_name())
1154 ea = cPersonNameEAPnl(self, -1, identity = self.__identity)
1155 dlg = gmEditArea.cGenericEditAreaDlg2(self, -1, edit_area = ea, single_entry = True)
1156 dlg.SetTitle(_('Adding new name'))
1157 if dlg.ShowModal() == wx.ID_OK:
1158 dlg.Destroy()
1159 return True
1160 dlg.Destroy()
1161 return False
1162 #--------------------------------------------------------
1164 ea = cPersonNameEAPnl(self, -1, name = name)
1165 dlg = gmEditArea.cGenericEditAreaDlg2(self, -1, edit_area = ea, single_entry = True)
1166 dlg.SetTitle(_('Cloning name'))
1167 if dlg.ShowModal() == wx.ID_OK:
1168 dlg.Destroy()
1169 return True
1170 dlg.Destroy()
1171 return False
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 # properties
1198 #--------------------------------------------------------
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 """
1214
1215 try:
1216 self.__identity = kwargs['identity']
1217 del kwargs['identity']
1218 except KeyError:
1219 self.__identity = None
1220
1221 gmListWidgets.cGenericListManagerPnl.__init__(self, *args, **kwargs)
1222
1223 self.new_callback = self._add_id
1224 self.edit_callback = self._edit_id
1225 self.delete_callback = self._del_id
1226 self.refresh_callback = self.refresh
1227
1228 self.__init_ui()
1229 self.refresh()
1230 #--------------------------------------------------------
1231 # external API
1232 #--------------------------------------------------------
1234 if self.__identity is None:
1235 self._LCTRL_items.set_string_items()
1236 return
1237
1238 ids = self.__identity.get_external_ids()
1239 self._LCTRL_items.set_string_items (
1240 items = [ [
1241 i['name'],
1242 i['value'],
1243 gmTools.coalesce(i['issuer'], u''),
1244 gmTools.coalesce(i['comment'], u'')
1245 ] for i in ids
1246 ]
1247 )
1248 self._LCTRL_items.set_column_widths()
1249 self._LCTRL_items.set_data(data = ids)
1250 #--------------------------------------------------------
1251 # internal helpers
1252 #--------------------------------------------------------
1254 self._LCTRL_items.set_columns(columns = [
1255 _('ID type'),
1256 _('Value'),
1257 _('Issuer'),
1258 _('Comment')
1259 ])
1260 #--------------------------------------------------------
1262 ea = cExternalIDEditAreaPnl(self, -1)
1263 ea.identity = self.__identity
1264 dlg = gmEditArea.cGenericEditAreaDlg2(self, -1, edit_area = ea)
1265 dlg.SetTitle(_('Adding new external ID'))
1266 if dlg.ShowModal() == wx.ID_OK:
1267 dlg.Destroy()
1268 return True
1269 dlg.Destroy()
1270 return False
1271 #--------------------------------------------------------
1273 ea = cExternalIDEditAreaPnl(self, -1, external_id = ext_id)
1274 ea.identity = self.__identity
1275 dlg = gmEditArea.cGenericEditAreaDlg2(self, -1, edit_area = ea, single_entry = True)
1276 dlg.SetTitle(_('Editing external ID'))
1277 if dlg.ShowModal() == wx.ID_OK:
1278 dlg.Destroy()
1279 return True
1280 dlg.Destroy()
1281 return False
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 # properties
1295 #--------------------------------------------------------
1298
1302
1303 identity = property(_get_identity, _set_identity)
1304 #------------------------------------------------------------
1305 # integrated panels
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 """
1320
1321 wxgPersonIdentityManagerPnl.wxgPersonIdentityManagerPnl.__init__(self, *args, **kwargs)
1322
1323 self.__identity = None
1324 self.refresh()
1325 #--------------------------------------------------------
1326 # external API
1327 #--------------------------------------------------------
1329 self._PNL_names.identity = self.__identity
1330 self._PNL_ids.identity = self.__identity
1331 # this is an Edit Area:
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 # properties
1339 #--------------------------------------------------------
1342
1346
1347 identity = property(_get_identity, _set_identity)
1348 #--------------------------------------------------------
1349 # event handlers
1350 #--------------------------------------------------------
1354 #self._PNL_identity.refresh()
1355 #--------------------------------------------------------
1358
1359 #============================================================
1360 from Gnumed.wxGladeWidgets import wxgPersonSocialNetworkManagerPnl
1361
1362 -class cPersonSocialNetworkManagerPnl(wxgPersonSocialNetworkManagerPnl.wxgPersonSocialNetworkManagerPnl):
1364
1365 wxgPersonSocialNetworkManagerPnl.wxgPersonSocialNetworkManagerPnl.__init__(self, *args, **kwargs)
1366
1367 self.__identity = None
1368 self._PRW_provider.selection_only = False
1369 self.refresh()
1370 #--------------------------------------------------------
1371 # external API
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 # properties
1412 #--------------------------------------------------------
1415
1419
1420 identity = property(_get_identity, _set_identity)
1421 #--------------------------------------------------------
1422 # event handlers
1423 #--------------------------------------------------------
1438 #--------------------------------------------------------
1441 #--------------------------------------------------------
1452 #--------------------------------------------------------
1460 #============================================================
1461 # new-patient widgets
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 # internal helpers
1538 #----------------------------------------------------------------
1540 self._PRW_lastname.final_regex = '.+'
1541 self._PRW_firstnames.final_regex = '.+'
1542 self._PRW_address_searcher.selection_only = False
1543
1544 # only if we would support None on selection_only's:
1545 # self._PRW_external_id_type.selection_only = True
1546
1547 if self.default_country is not None:
1548 match = self._PRW_country._data2match(data = self.default_country)
1549 if match is not None:
1550 self._PRW_country.SetText(value = match['field_label'], data = match['data'])
1551
1552 if self.default_region is not None:
1553 self._PRW_region.SetText(value = self.default_region)
1554 #----------------------------------------------------------------
1556
1557 adr = self._PRW_address_searcher.address
1558 if adr is None:
1559 return True
1560
1561 if ctrl.GetValue().strip() != adr[field]:
1562 wx.CallAfter(self._PRW_address_searcher.SetText, value = u'', data = None)
1563 return True
1564
1565 return False
1566 #----------------------------------------------------------------
1568 adr = self._PRW_address_searcher.address
1569 if adr is None:
1570 return True
1571
1572 self._PRW_zip.SetText(value = adr['postcode'], data = adr['postcode'])
1573
1574 self._PRW_street.SetText(value = adr['street'], data = adr['street'])
1575 self._PRW_street.set_context(context = u'zip', val = adr['postcode'])
1576
1577 self._PRW_urb.SetText(value = adr['urb'], data = adr['urb'])
1578 self._PRW_urb.set_context(context = u'zip', val = adr['postcode'])
1579
1580 self._PRW_region.SetText(value = adr['l10n_state'], data = adr['code_state'])
1581 self._PRW_region.set_context(context = u'zip', val = adr['postcode'])
1582
1583 self._PRW_country.SetText(value = adr['l10n_country'], data = adr['code_country'])
1584 self._PRW_country.set_context(context = u'zip', val = adr['postcode'])
1585 #----------------------------------------------------------------
1587 error = False
1588
1589 # name fields
1590 if self._PRW_lastname.GetValue().strip() == u'':
1591 error = True
1592 gmDispatcher.send(signal = 'statustext', msg = _('Must enter lastname.'))
1593 self._PRW_lastname.display_as_valid(False)
1594 else:
1595 self._PRW_lastname.display_as_valid(True)
1596
1597 if self._PRW_firstnames.GetValue().strip() == '':
1598 error = True
1599 gmDispatcher.send(signal = 'statustext', msg = _('Must enter first name.'))
1600 self._PRW_firstnames.display_as_valid(False)
1601 else:
1602 self._PRW_firstnames.display_as_valid(True)
1603
1604 # gender
1605 if self._PRW_gender.GetData() is None:
1606 error = True
1607 gmDispatcher.send(signal = 'statustext', msg = _('Must select gender.'))
1608 self._PRW_gender.display_as_valid(False)
1609 else:
1610 self._PRW_gender.display_as_valid(True)
1611
1612 # dob validation
1613 if not _validate_dob_field(self._PRW_dob):
1614 error = True
1615
1616 # TOB validation if non-empty
1617 # if self._TCTRL_tob.GetValue().strip() != u'':
1618
1619 return (not error)
1620 #----------------------------------------------------------------
1622
1623 # existing address ? if so set other fields
1624 if self._PRW_address_searcher.GetData() is not None:
1625 wx.CallAfter(self.__set_fields_from_address_searcher)
1626 return True
1627
1628 # must either all contain something or none of them
1629 fields_to_fill = (
1630 self._TCTRL_number,
1631 self._PRW_zip,
1632 self._PRW_street,
1633 self._PRW_urb,
1634 self._PRW_type
1635 )
1636 no_of_filled_fields = 0
1637
1638 for field in fields_to_fill:
1639 if field.GetValue().strip() != u'':
1640 no_of_filled_fields += 1
1641 field.SetBackgroundColour(gmPhraseWheel.color_prw_valid)
1642 field.Refresh()
1643
1644 # empty address ?
1645 if no_of_filled_fields == 0:
1646 if empty_address_is_valid:
1647 return True
1648 else:
1649 return None
1650
1651 # incompletely filled address ?
1652 if no_of_filled_fields != len(fields_to_fill):
1653 for field in fields_to_fill:
1654 if field.GetValue().strip() == u'':
1655 field.SetBackgroundColour(gmPhraseWheel.color_prw_invalid)
1656 field.SetFocus()
1657 field.Refresh()
1658 msg = _('To properly create an address, all the related fields must be filled in.')
1659 gmGuiHelpers.gm_show_error(msg, _('Required fields'))
1660 return False
1661
1662 # fields which must contain a selected item
1663 # FIXME: they must also contain an *acceptable combination* which
1664 # FIXME: can only be tested against the database itself ...
1665 strict_fields = (
1666 self._PRW_type,
1667 self._PRW_region,
1668 self._PRW_country
1669 )
1670 error = False
1671 for field in strict_fields:
1672 if field.GetData() is None:
1673 error = True
1674 field.SetBackgroundColour(gmPhraseWheel.color_prw_invalid)
1675 field.SetFocus()
1676 else:
1677 field.SetBackgroundColour(gmPhraseWheel.color_prw_valid)
1678 field.Refresh()
1679
1680 if error:
1681 msg = _('This field must contain an item selected from the dropdown list.')
1682 gmGuiHelpers.gm_show_error(msg, _('Required fields'))
1683 return False
1684
1685 return True
1686 #----------------------------------------------------------------
1688
1689 # identity
1690 self._PRW_firstnames.add_callback_on_lose_focus(self._on_leaving_firstname)
1691
1692 # address
1693 self._PRW_address_searcher.add_callback_on_lose_focus(self._on_leaving_adress_searcher)
1694
1695 # invalidate address searcher when any field edited
1696 self._PRW_street.add_callback_on_lose_focus(self._invalidate_address_searcher)
1697 wx.EVT_KILL_FOCUS(self._TCTRL_number, self._on_leaving_number)
1698 wx.EVT_KILL_FOCUS(self._TCTRL_unit, self._on_leaving_unit)
1699 self._PRW_urb.add_callback_on_lose_focus(self._invalidate_address_searcher)
1700 self._PRW_region.add_callback_on_lose_focus(self._invalidate_address_searcher)
1701
1702 self._PRW_zip.add_callback_on_lose_focus(self._on_leaving_zip)
1703 self._PRW_country.add_callback_on_lose_focus(self._on_leaving_country)
1704 #----------------------------------------------------------------
1705 # event handlers
1706 #----------------------------------------------------------------
1708 """Set the gender according to entered firstname.
1709
1710 Matches are fetched from existing records in backend.
1711 """
1712 # only set if not already set so as to not
1713 # overwrite a change by the user
1714 if self._PRW_gender.GetData() is not None:
1715 return True
1716
1717 firstname = self._PRW_firstnames.GetValue().strip()
1718 if firstname == u'':
1719 return True
1720
1721 gender = gmPerson.map_firstnames2gender(firstnames = firstname)
1722 if gender is None:
1723 return True
1724
1725 wx.CallAfter(self._PRW_gender.SetData, gender)
1726 return True
1727 #----------------------------------------------------------------
1729 self.__perhaps_invalidate_address_searcher(self._PRW_zip, 'postcode')
1730
1731 zip_code = gmTools.none_if(self._PRW_zip.GetValue().strip(), u'')
1732 self._PRW_street.set_context(context = u'zip', val = zip_code)
1733 self._PRW_urb.set_context(context = u'zip', val = zip_code)
1734 self._PRW_region.set_context(context = u'zip', val = zip_code)
1735 self._PRW_country.set_context(context = u'zip', val = zip_code)
1736
1737 return True
1738 #----------------------------------------------------------------
1740 self.__perhaps_invalidate_address_searcher(self._PRW_country, 'l10n_country')
1741
1742 country = gmTools.none_if(self._PRW_country.GetValue().strip(), u'')
1743 self._PRW_region.set_context(context = u'country', val = country)
1744
1745 return True
1746 #----------------------------------------------------------------
1748 if self._TCTRL_number.GetValue().strip() == u'':
1749 adr = self._PRW_address_searcher.address
1750 if adr is None:
1751 return True
1752 self._TCTRL_number.SetValue(adr['number'])
1753 return True
1754
1755 self.__perhaps_invalidate_address_searcher(self._TCTRL_number, 'number')
1756 return True
1757 #----------------------------------------------------------------
1759 if self._TCTRL_unit.GetValue().strip() == u'':
1760 adr = self._PRW_address_searcher.address
1761 if adr is None:
1762 return True
1763 self._TCTRL_unit.SetValue(gmTools.coalesce(adr['subunit'], u''))
1764 return True
1765
1766 self.__perhaps_invalidate_address_searcher(self._TCTRL_numbunit, 'subunit')
1767 return True
1768 #----------------------------------------------------------------
1770 mapping = [
1771 (self._PRW_street, 'street'),
1772 (self._PRW_urb, 'urb'),
1773 (self._PRW_region, 'l10n_state')
1774 ]
1775 # loop through fields and invalidate address searcher if different
1776 for ctrl, field in mapping:
1777 if self.__perhaps_invalidate_address_searcher(ctrl, field):
1778 return True
1779
1780 return True
1781 #----------------------------------------------------------------
1783 if self._PRW_address_searcher.address is None:
1784 return True
1785
1786 wx.CallAfter(self.__set_fields_from_address_searcher)
1787 return True
1788 #----------------------------------------------------------------
1789 # generic Edit Area mixin API
1790 #----------------------------------------------------------------
1792 if self._PRW_primary_provider.GetValue().strip() == u'':
1793 self._PRW_primary_provider.display_as_valid(True)
1794 else:
1795 if self._PRW_primary_provider.GetData() is None:
1796 self._PRW_primary_provider.display_as_valid(False)
1797 else:
1798 self._PRW_primary_provider.display_as_valid(True)
1799 return (self.__identity_valid_for_save() and self.__address_valid_for_save(empty_address_is_valid = True))
1800 #----------------------------------------------------------------
1802
1803 if self._PRW_dob.GetValue().strip() == u'':
1804 if not _empty_dob_allowed():
1805 self._PRW_dob.display_as_valid(False)
1806 self._PRW_dob.SetFocus()
1807 return False
1808
1809 # identity
1810 new_identity = gmPerson.create_identity (
1811 gender = self._PRW_gender.GetData(),
1812 dob = self._PRW_dob.GetData(),
1813 lastnames = self._PRW_lastname.GetValue().strip(),
1814 firstnames = self._PRW_firstnames.GetValue().strip()
1815 )
1816 _log.debug('identity created: %s' % new_identity)
1817
1818 new_identity['title'] = gmTools.none_if(self._PRW_title.GetValue().strip())
1819 new_identity.set_nickname(nickname = gmTools.none_if(self._PRW_nickname.GetValue().strip(), u''))
1820 #TOB
1821 prov = self._PRW_primary_provider.GetData()
1822 if prov is not None:
1823 new_identity['pk_primary_provider'] = prov
1824 new_identity['comment'] = gmTools.none_if(self._TCTRL_comment.GetValue().strip(), u'')
1825 new_identity.save()
1826
1827 # address
1828 # if we reach this the address cannot be completely empty
1829 is_valid = self.__address_valid_for_save(empty_address_is_valid = False)
1830 if is_valid is True:
1831 # because we currently only check for non-emptiness
1832 # we must still deal with database errors
1833 try:
1834 new_identity.link_address (
1835 number = self._TCTRL_number.GetValue().strip(),
1836 street = self._PRW_street.GetValue().strip(),
1837 postcode = self._PRW_zip.GetValue().strip(),
1838 urb = self._PRW_urb.GetValue().strip(),
1839 state = self._PRW_region.GetData(),
1840 country = self._PRW_country.GetData(),
1841 subunit = gmTools.none_if(self._TCTRL_unit.GetValue().strip(), u''),
1842 id_type = self._PRW_type.GetData()
1843 )
1844 except gmPG2.dbapi.InternalError:
1845 _log.debug('number: >>%s<<', self._TCTRL_number.GetValue().strip())
1846 _log.debug('(sub)unit: >>%s<<', self._TCTRL_unit.GetValue().strip())
1847 _log.debug('street: >>%s<<', self._PRW_street.GetValue().strip())
1848 _log.debug('postcode: >>%s<<', self._PRW_zip.GetValue().strip())
1849 _log.debug('urb: >>%s<<', self._PRW_urb.GetValue().strip())
1850 _log.debug('state: >>%s<<', self._PRW_region.GetData().strip())
1851 _log.debug('country: >>%s<<', self._PRW_country.GetData().strip())
1852 _log.exception('cannot link address')
1853 gmGuiHelpers.gm_show_error (
1854 aTitle = _('Saving address'),
1855 aMessage = _(
1856 'Cannot save this address.\n'
1857 '\n'
1858 'You will have to add it via the Demographics plugin.\n'
1859 )
1860 )
1861 elif is_valid is False:
1862 gmGuiHelpers.gm_show_error (
1863 aTitle = _('Saving address'),
1864 aMessage = _(
1865 'Address not saved.\n'
1866 '\n'
1867 'You will have to add it via the Demographics plugin.\n'
1868 )
1869 )
1870 # else it is None which means empty address which we ignore
1871
1872 # phone
1873 channel_name = self._PRW_channel_type.GetValue().strip()
1874 pk_channel_type = self._PRW_channel_type.GetData()
1875 if pk_channel_type is None:
1876 if channel_name == u'':
1877 channel_name = u'homephone'
1878 new_identity.link_comm_channel (
1879 comm_medium = channel_name,
1880 pk_channel_type = pk_channel_type,
1881 url = gmTools.none_if(self._TCTRL_phone.GetValue().strip(), u''),
1882 is_confidential = False
1883 )
1884
1885 # external ID
1886 pk_type = self._PRW_external_id_type.GetData()
1887 id_value = self._TCTRL_external_id_value.GetValue().strip()
1888 if (pk_type is not None) and (id_value != u''):
1889 new_identity.add_external_id(value = id_value, pk_type = pk_type)
1890
1891 # occupation
1892 new_identity.link_occupation (
1893 occupation = gmTools.none_if(self._PRW_occupation.GetValue().strip(), u'')
1894 )
1895
1896 self.data = new_identity
1897 return True
1898 #----------------------------------------------------------------
1901 #----------------------------------------------------------------
1905 #----------------------------------------------------------------
1908 #----------------------------------------------------------------
1911
1912 #============================================================
1913 # patient demographics editing classes
1914 #============================================================
1916 """Notebook displaying demographics editing pages:
1917
1918 - Contacts (addresses, phone numbers, etc)
1919 - Identity
1920 - Social network (significant others, GP, etc)
1921
1922 Does NOT act on/listen to the current patient.
1923 """
1924 #--------------------------------------------------------
1926
1927 wx.Notebook.__init__ (
1928 self,
1929 parent = parent,
1930 id = id,
1931 style = wx.NB_TOP | wx.NB_MULTILINE | wx.NO_BORDER,
1932 name = self.__class__.__name__
1933 )
1934
1935 self.__identity = None
1936 self.__do_layout()
1937 self.SetSelection(0)
1938 #--------------------------------------------------------
1939 # public API
1940 #--------------------------------------------------------
1942 """Populate fields in pages with data from model."""
1943 for page_idx in range(self.GetPageCount()):
1944 page = self.GetPage(page_idx)
1945 page.identity = self.__identity
1946
1947 return True
1948 #--------------------------------------------------------
1949 # internal API
1950 #--------------------------------------------------------
1952 """Build patient edition notebook pages."""
1953
1954 # contacts page
1955 new_page = gmPersonContactWidgets.cPersonContactsManagerPnl(self, -1)
1956 new_page.identity = self.__identity
1957 self.AddPage (
1958 page = new_page,
1959 text = _('Contacts'),
1960 select = True
1961 )
1962
1963 # identity page
1964 new_page = cPersonIdentityManagerPnl(self, -1)
1965 new_page.identity = self.__identity
1966 self.AddPage (
1967 page = new_page,
1968 text = _('Identity'),
1969 select = False
1970 )
1971
1972 # social network page
1973 new_page = cPersonSocialNetworkManagerPnl(self, -1)
1974 new_page.identity = self.__identity
1975 self.AddPage (
1976 page = new_page,
1977 text = _('Social network'),
1978 select = False
1979 )
1980 #--------------------------------------------------------
1981 # properties
1982 #--------------------------------------------------------
1985
1987 self.__identity = identity
1988
1989 identity = property(_get_identity, _set_identity)
1990 #============================================================
1991 # old occupation widgets
1992 #============================================================
1993 # FIXME: support multiple occupations
1994 # FIXME: redo with wxGlade
1995
1997 """Page containing patient occupations edition fields.
1998 """
2000 """
2001 Creates a new instance of BasicPatDetailsPage
2002 @param parent - The parent widget
2003 @type parent - A wx.Window instance
2004 @param id - The widget id
2005 @type id - An integer
2006 """
2007 wx.Panel.__init__(self, parent, id)
2008 self.__ident = ident
2009 self.__do_layout()
2010 #--------------------------------------------------------
2012 PNL_form = wx.Panel(self, -1)
2013 # occupation
2014 STT_occupation = wx.StaticText(PNL_form, -1, _('Occupation'))
2015 self.PRW_occupation = cOccupationPhraseWheel(parent = PNL_form, id = -1)
2016 self.PRW_occupation.SetToolTipString(_("primary occupation of the patient"))
2017 # known since
2018 STT_occupation_updated = wx.StaticText(PNL_form, -1, _('Last updated'))
2019 self.TTC_occupation_updated = wx.TextCtrl(PNL_form, -1, style = wx.TE_READONLY)
2020
2021 # layout input widgets
2022 SZR_input = wx.FlexGridSizer(cols = 2, rows = 5, vgap = 4, hgap = 4)
2023 SZR_input.AddGrowableCol(1)
2024 SZR_input.Add(STT_occupation, 0, wx.SHAPED)
2025 SZR_input.Add(self.PRW_occupation, 1, wx.EXPAND)
2026 SZR_input.Add(STT_occupation_updated, 0, wx.SHAPED)
2027 SZR_input.Add(self.TTC_occupation_updated, 1, wx.EXPAND)
2028 PNL_form.SetSizerAndFit(SZR_input)
2029
2030 # layout page
2031 SZR_main = wx.BoxSizer(wx.VERTICAL)
2032 SZR_main.Add(PNL_form, 1, wx.EXPAND)
2033 self.SetSizer(SZR_main)
2034 #--------------------------------------------------------
2037 #--------------------------------------------------------
2039 if identity is not None:
2040 self.__ident = identity
2041 jobs = self.__ident.get_occupations()
2042 if len(jobs) > 0:
2043 self.PRW_occupation.SetText(jobs[0]['l10n_occupation'])
2044 self.TTC_occupation_updated.SetValue(jobs[0]['modified_when'].strftime('%m/%Y'))
2045 return True
2046 #--------------------------------------------------------
2048 if self.PRW_occupation.IsModified():
2049 new_job = self.PRW_occupation.GetValue().strip()
2050 jobs = self.__ident.get_occupations()
2051 for job in jobs:
2052 if job['l10n_occupation'] == new_job:
2053 continue
2054 self.__ident.unlink_occupation(occupation = job['l10n_occupation'])
2055 self.__ident.link_occupation(occupation = new_job)
2056 return True
2057 #============================================================
2059 """Patient demographics plugin for main notebook.
2060
2061 Hosts another notebook with pages for Identity, Contacts, etc.
2062
2063 Acts on/listens to the currently active patient.
2064 """
2065 #--------------------------------------------------------
2067 wx.Panel.__init__ (self, parent = parent, id = id, style = wx.NO_BORDER)
2068 gmRegetMixin.cRegetOnPaintMixin.__init__(self)
2069 self.__do_layout()
2070 self.__register_interests()
2071 #--------------------------------------------------------
2072 # public API
2073 #--------------------------------------------------------
2074 #--------------------------------------------------------
2075 # internal helpers
2076 #--------------------------------------------------------
2078 """Arrange widgets."""
2079 self.__patient_notebook = cPersonDemographicsEditorNb(self, -1)
2080
2081 szr_main = wx.BoxSizer(wx.VERTICAL)
2082 szr_main.Add(self.__patient_notebook, 1, wx.EXPAND)
2083 self.SetSizerAndFit(szr_main)
2084 #--------------------------------------------------------
2085 # event handling
2086 #--------------------------------------------------------
2088 gmDispatcher.connect(signal = u'pre_patient_selection', receiver = self._on_pre_patient_selection)
2089 gmDispatcher.connect(signal = u'post_patient_selection', receiver = self._on_post_patient_selection)
2090 #--------------------------------------------------------
2093 #--------------------------------------------------------
2097 # reget mixin API
2098 #--------------------------------------------------------
2108 #============================================================
2109 #============================================================
2110 if __name__ == "__main__":
2111
2112 #--------------------------------------------------------
2114 app = wx.PyWidgetTester(size = (600, 400))
2115 app.SetWidget(cKOrganizerSchedulePnl)
2116 app.MainLoop()
2117 #--------------------------------------------------------
2119 app = wx.PyWidgetTester(size = (600, 400))
2120 widget = cPersonNamesManagerPnl(app.frame, -1)
2121 widget.identity = activate_patient()
2122 app.frame.Show(True)
2123 app.MainLoop()
2124 #--------------------------------------------------------
2126 app = wx.PyWidgetTester(size = (600, 400))
2127 widget = cPersonIDsManagerPnl(app.frame, -1)
2128 widget.identity = activate_patient()
2129 app.frame.Show(True)
2130 app.MainLoop()
2131 #--------------------------------------------------------
2133 app = wx.PyWidgetTester(size = (600, 400))
2134 widget = cPersonIdentityManagerPnl(app.frame, -1)
2135 widget.identity = activate_patient()
2136 app.frame.Show(True)
2137 app.MainLoop()
2138 #--------------------------------------------------------
2140 app = wx.PyWidgetTester(size = (600, 400))
2141 app.SetWidget(cPersonNameEAPnl, name = activate_patient().get_active_name())
2142 app.MainLoop()
2143 #--------------------------------------------------------
2145 app = wx.PyWidgetTester(size = (600, 400))
2146 widget = cPersonDemographicsEditorNb(app.frame, -1)
2147 widget.identity = activate_patient()
2148 widget.refresh()
2149 app.frame.Show(True)
2150 app.MainLoop()
2151 #--------------------------------------------------------
2153 patient = gmPersonSearch.ask_for_patient()
2154 if patient is None:
2155 print "No patient. Exiting gracefully..."
2156 sys.exit(0)
2157 from Gnumed.wxpython import gmPatSearchWidgets
2158 gmPatSearchWidgets.set_active_patient(patient=patient)
2159 return patient
2160 #--------------------------------------------------------
2161 if len(sys.argv) > 1 and sys.argv[1] == 'test':
2162
2163 gmI18N.activate_locale()
2164 gmI18N.install_domain(domain='gnumed')
2165 gmPG2.get_connection()
2166
2167 # app = wx.PyWidgetTester(size = (400, 300))
2168 # app.SetWidget(cNotebookedPatEditionPanel, -1)
2169 # app.frame.Show(True)
2170 # app.MainLoop()
2171
2172 # phrasewheels
2173 # test_organizer_pnl()
2174
2175 # identity related widgets
2176 #test_person_names_pnl()
2177 test_person_ids_pnl()
2178 #test_pat_ids_pnl()
2179 #test_name_ea_pnl()
2180
2181 #test_cPersonDemographicsEditorNb()
2182
2183 #============================================================
2184
| Home | Trees | Indices | Help |
|
|---|
| Generated by Epydoc 3.0.1 on Tue Oct 18 04:00:25 2011 | http://epydoc.sourceforge.net |