- properly validate new-patient contact data
[gnumed:gnumed-fedora.git] / gnumed / gnumed / client / wxpython / gmDemographicsWidgets.py
1 """Widgets dealing with patient demographics."""
2 #============================================================
3 # $Source: /home/ncq/Projekte/cvs2git/vcs-mirror/gnumed/gnumed/client/wxpython/gmDemographicsWidgets.py,v $
4 # $Id: gmDemographicsWidgets.py,v 1.137.2.3 2008-06-02 14:15:49 ncq Exp $
5 __version__ = "$Revision: 1.137.2.3 $"
6 __author__ = "R.Terry, SJ Tan, I Haywood, Carlos Moro <cfmoro1976@yahoo.es>"
7 __license__ = 'GPL (details at http://www.gnu.org)'
8
9 # standard library
10 import time, string, sys, os, datetime as pyDT, csv, codecs, re as regex
11
12
13 import wx
14 import wx.wizard
15
16
17 # GNUmed specific
18 if __name__ == '__main__':
19         sys.path.insert(0, '../../')
20 from Gnumed.wxpython import gmPlugin, gmPhraseWheel, gmGuiHelpers, gmDateTimeInput, gmRegetMixin, gmDataMiningWidgets, gmListWidgets, gmEditArea, gmAuthWidgets
21 from Gnumed.pycommon import gmGuiBroker, gmLog, gmDispatcher, gmSignals, gmCfg, gmI18N, gmMatchProvider, gmPG2, gmTools, gmDateTime, gmShellAPI
22 from Gnumed.business import gmDemographicRecord, gmPerson
23 from Gnumed.wxGladeWidgets import wxgGenericAddressEditAreaPnl, wxgPersonContactsManagerPnl, wxgPersonIdentityManagerPnl, wxgNameGenderDOBEditAreaPnl, wxgCommChannelEditAreaPnl, wxgExternalIDEditAreaPnl
24
25
26 # constant defs
27 _log = gmLog.gmDefLog
28 _cfg = gmCfg.gmDefCfgFile
29
30
31 try:
32         _('do-not-translate-but-make-epydoc-happy')
33 except NameError:
34         _ = lambda x:x
35
36 #============================================================
37 class cKOrganizerSchedulePnl(gmDataMiningWidgets.cPatientListingPnl):
38
39         def __init__(self, *args, **kwargs):
40
41                 kwargs['message'] = _("Today's KOrganizer appointments ...")
42                 kwargs['button_defs'] = [
43                         {'label': _('Reload'), 'tooltip': _('Reload appointments from KOrganizer')},
44                         {'label': u''},
45                         {'label': u''},
46                         {'label': u''},
47                         {'label': u'KOrganizer', 'tooltip': _('Launch KOrganizer')}
48                 ]
49                 gmDataMiningWidgets.cPatientListingPnl.__init__(self, *args, **kwargs)
50
51                 self.fname = os.path.expanduser(os.path.join('~', '.gnumed', 'tmp', 'korganizer2gnumed.csv'))
52                 self.reload_cmd = 'konsolekalendar --view --export-type csv --export-file %s' % self.fname
53
54         #--------------------------------------------------------
55         def _on_BTN_1_pressed(self, event):
56                 """Reload appointments from KOrganizer."""
57                 self.reload_appointments()
58         #--------------------------------------------------------
59         def _on_BTN_5_pressed(self, event):
60                 """Reload appointments from KOrganizer."""
61                 gmShellAPI.run_command_in_shell(command = 'korganizer', blocking = False)
62         #--------------------------------------------------------
63         def reload_appointments(self):
64                 try: os.remove(self.fname)
65                 except OSError: pass
66                 gmShellAPI.run_command_in_shell(command=self.reload_cmd, blocking=True)
67                 try:
68                         csv_file = codecs.open(self.fname , mode = 'rU', encoding = 'utf8', errors = 'replace')
69                 except IOError:
70                         gmDispatcher.send(signal = u'statustext', msg = _('Cannot access KOrganizer transfer file [%s]') % self.fname, beep = True)
71                         return
72
73                 csv_lines = gmTools.unicode_csv_reader (
74                         csv_file,
75                         delimiter = ','
76                 )
77                 # start_date, start_time, end_date, end_time, title (patient), ort, comment, UID
78                 self._LCTRL_items.set_columns ([
79                         _('Place'),
80                         _('Start'),
81                         u'',
82                         u'',
83                         _('Patient'),
84                         _('Comment')
85                 ])
86                 items = []
87                 data = []
88                 for line in csv_lines:
89                         items.append([line[5], line[0], line[1], line[3], line[4], line[6]])
90                         data.append([line[4], line[7]])
91
92                 self._LCTRL_items.set_string_items(items = items)
93                 self._LCTRL_items.set_column_widths()
94                 self._LCTRL_items.set_data(data = data)
95                 self._LCTRL_items.patient_key = 0
96         #--------------------------------------------------------
97         # notebook plugins API
98         #--------------------------------------------------------
99         def repopulate_ui(self):
100                 self.reload_appointments()
101 #============================================================
102 def edit_occupation():
103
104         pat = gmPerson.gmCurrentPatient()
105         curr_jobs = pat.get_occupations()
106         if len(curr_jobs) > 0:
107                 old_job = curr_jobs[0]['l10n_occupation']
108                 update = curr_jobs[0]['modified_when'].strftime('%m/%Y')
109         else:
110                 old_job = u''
111                 update = u''
112
113         msg = _(
114                 'Please enter the primary occupation of the patient.\n'
115                 '\n'
116                 'Currently recorded:\n'
117                 '\n'
118                 ' %s (last updated %s)'
119         ) % (old_job, update)
120
121         new_job = wx.GetTextFromUser (
122                 message = msg,
123                 caption = _('Editing primary occupation'),
124                 default_value = old_job,
125                 parent = None
126         )
127         if new_job.strip() == u'':
128                 return
129
130         for job in curr_jobs:
131                 # unlink all but the new job
132                 if job['l10n_occupation'] != new_job:
133                         pat.unlink_occupation(occupation = job['l10n_occupation'])
134         # and link the new one
135         pat.link_occupation(occupation = new_job)
136 #============================================================
137 def disable_identity(identity=None):
138         # ask user for assurance
139         go_ahead = gmGuiHelpers.gm_show_question (
140                 _('Are you sure you really, positively want\n'
141                   'to disable the following patient ?\n'
142                   '\n'
143                   ' %s %s %s\n'
144                   ' born %s\n'
145                 ) % (
146                         identity['firstnames'],
147                         identity['lastnames'],
148                         identity['gender'],
149                         identity['dob']
150                 ),
151                 _('Disabling patient')
152         )
153         if not go_ahead:
154                 return True
155
156         # get admin connection
157         conn = gmAuthWidgets.get_dbowner_connection (
158                 procedure = _('Disabling patient')
159         )
160         # - user cancelled
161         if conn is False:
162                 return True
163         # - error
164         if conn is None:
165                 return False
166
167         # now disable patient
168         gmPG2.run_rw_queries(queries = [{'cmd': u"update dem.identity set deleted=True where pk=%s", 'args': [identity['pk_identity']]}])
169
170         return True
171 #============================================================
172 # address phrasewheels and widgets
173 #============================================================
174 class cPersonAddressesManagerPnl(gmListWidgets.cGenericListManagerPnl):
175         """A list for managing a person's addresses.
176
177         Does NOT act on/listen to the current patient.
178         """
179         def __init__(self, *args, **kwargs):
180
181                 try:
182                         self.__identity = kwargs['identity']
183                         del kwargs['identity']
184                 except KeyError:
185                         self.__identity = None
186
187                 gmListWidgets.cGenericListManagerPnl.__init__(self, *args, **kwargs)
188
189                 self.new_callback = self._add_address
190                 self.edit_callback = self._edit_address
191                 self.delete_callback = self._del_address
192                 self.refresh_callback = self.refresh
193
194                 self.__init_ui()
195                 self.refresh()
196         #--------------------------------------------------------
197         # external API
198         #--------------------------------------------------------
199         def refresh(self, *args, **kwargs):
200                 if self.__identity is None:
201                         self._LCTRL_items.set_string_items()
202                         return
203
204                 adrs = self.__identity.get_addresses()
205                 self._LCTRL_items.set_string_items (
206                         items = [ [
207                                         a['l10n_address_type'],
208                                         a['street'],
209                                         gmTools.coalesce(a['notes_street'], u''),
210                                         a['number'],
211                                         gmTools.coalesce(a['subunit'], u''),
212                                         a['postcode'],
213                                         a['urb'],
214                                         gmTools.coalesce(a['suburb'], u''),
215                                         a['l10n_state'],
216                                         a['l10n_country'],
217                                         gmTools.coalesce(a['notes_subunit'], u'')
218                                 ] for a in adrs
219                         ]
220                 )
221                 self._LCTRL_items.set_column_widths()
222                 self._LCTRL_items.set_data(data = adrs)
223         #--------------------------------------------------------
224         # internal helpers
225         #--------------------------------------------------------
226         def __init_ui(self):
227                 self._LCTRL_items.set_columns(columns = [
228                         _('Type'),
229                         _('Street'),
230                         _('Directions'),
231                         _('Number'),
232                         _('Subunit'),
233                         _('Postcode'),
234                         _('Town'),
235                         _('Suburb'),
236                         _('State'),
237                         _('Country'),
238                         _('Comment')
239                 ])
240         #--------------------------------------------------------
241         def _add_address(self):
242                 ea = cAddressEditAreaPnl(self, -1)
243                 ea.identity = self.__identity
244                 dlg = gmEditArea.cGenericEditAreaDlg(self, -1, edit_area = ea)
245                 dlg.SetTitle(_('Adding new address'))
246                 if dlg.ShowModal() == wx.ID_OK:
247                         return True
248                 return False
249         #--------------------------------------------------------
250         def _edit_address(self, address):
251                 ea = cAddressEditAreaPnl(self, -1, address = address)
252                 ea.identity = self.__identity
253                 dlg = gmEditArea.cGenericEditAreaDlg(self, -1, edit_area = ea)
254                 dlg.SetTitle(_('Editing address'))
255                 if dlg.ShowModal() == wx.ID_OK:
256                         # did we add an entirely new address ?
257                         # if so then unlink the old one as implied by "edit"
258                         if ea.address['pk_address'] != address['pk_address']:
259                                 self.__identity.unlink_address(address = address)
260                         return True
261                 return False
262         #--------------------------------------------------------
263         def _del_address(self, address):
264                 go_ahead = gmGuiHelpers.gm_show_question (
265                         _(      'Are you sure you want to remove this\n'
266                                 "address from the patient's addresses ?\n"
267                                 '\n'
268                                 'The address itself will not be deleted\n'
269                                 'but it will no longer be associated with\n'
270                                 'this patient.'
271                         ),
272                         _('Removing address')
273                 )
274                 if not go_ahead:
275                         return False
276                 self.__identity.unlink_address(address = address)
277                 return True
278         #--------------------------------------------------------
279         # properties
280         #--------------------------------------------------------
281         def _get_identity(self):
282                 return self.__identity
283
284         def _set_identity(self, identity):
285                 self.__identity = identity
286                 self.refresh()
287
288         identity = property(_get_identity, _set_identity)
289 #============================================================
290 class cPersonContactsManagerPnl(wxgPersonContactsManagerPnl.wxgPersonContactsManagerPnl):
291         """A panel for editing contact data for a person.
292
293         - provides access to:
294           - addresses
295           - communication paths
296
297         Does NOT act on/listen to the current patient.
298         """
299         def __init__(self, *args, **kwargs):
300
301                 wxgPersonContactsManagerPnl.wxgPersonContactsManagerPnl.__init__(self, *args, **kwargs)
302
303                 self.__identity = None
304                 self.refresh()
305         #--------------------------------------------------------
306         # external API
307         #--------------------------------------------------------
308         def refresh(self):
309                 self._PNL_addresses.identity = self.__identity
310                 self._PNL_comms.identity = self.__identity
311         #--------------------------------------------------------
312         # properties
313         #--------------------------------------------------------
314         def _get_identity(self):
315                 return self.__identity
316
317         def _set_identity(self, identity):
318                 self.__identity = identity
319                 self.refresh()
320
321         identity = property(_get_identity, _set_identity)
322 #============================================================
323 class cAddressEditAreaPnl(wxgGenericAddressEditAreaPnl.wxgGenericAddressEditAreaPnl):
324         """An edit area for editing/creating an address.
325
326         Does NOT act on/listen to the current patient.
327         """
328         def __init__(self, *args, **kwargs):
329                 try:
330                         self.address = kwargs['address']
331                         del kwargs['address']
332                 except KeyError:
333                         self.address = None
334
335                 wxgGenericAddressEditAreaPnl.wxgGenericAddressEditAreaPnl.__init__(self, *args, **kwargs)
336
337                 self.identity = None
338
339                 self.__register_interests()
340                 self.refresh()
341         #--------------------------------------------------------
342         # external API
343         #--------------------------------------------------------
344         def refresh(self, address = None):
345                 if address is not None:
346                         self.address = address
347
348                 if self.address is not None:
349                         self._PRW_type.SetText(self.address['l10n_address_type'])
350                         self._PRW_zip.SetText(self.address['postcode'])
351                         self._PRW_street.SetText(self.address['street'], data = self.address['street'])
352                         self._TCTRL_notes_street.SetValue(gmTools.coalesce(self.address['notes_street'], ''))
353                         self._TCTRL_number.SetValue(self.address['number'])
354                         self._TCTRL_subunit.SetValue(gmTools.coalesce(self.address['subunit'], ''))
355                         self._PRW_suburb.SetText(gmTools.coalesce(self.address['suburb'], ''))
356                         self._PRW_urb.SetText(self.address['urb'], data = self.address['urb'])
357                         self._PRW_state.SetText(self.address['l10n_state'], data = self.address['code_state'])
358                         self._PRW_country.SetText(self.address['l10n_country'], data = self.address['code_country'])
359                         self._TCTRL_notes_subunit.SetValue(gmTools.coalesce(self.address['notes_subunit'], ''))
360                 # FIXME: clear fields
361 #               else:
362 #                       pass
363         #--------------------------------------------------------
364         def save(self):
365                 """Links address to patient, creating new address if necessary"""
366
367                 if not self.__valid_for_save():
368                         return False
369
370                 # link address to patient
371                 adr = self.identity.link_address (
372                         number = self._TCTRL_number.GetValue().strip(),
373                         street = self._PRW_street.GetValue().strip(),
374                         postcode = self._PRW_zip.GetValue().strip(),
375                         urb = self._PRW_urb.GetValue().strip(),
376                         state = self._PRW_state.GetData(),
377                         country = self._PRW_country.GetData(),
378                         subunit = gmTools.none_if(self._TCTRL_subunit.GetValue().strip(), u''),
379                         suburb = gmTools.none_if(self._PRW_suburb.GetValue().strip(), u''),
380                         id_type = self._PRW_type.GetData()
381                 )
382
383                 notes = self._TCTRL_notes_street.GetValue().strip()
384                 if notes != u'':
385                         adr['notes_street'] = notes
386                 notes = self._TCTRL_notes_subunit.GetValue().strip()
387                 if notes != u'':
388                         adr['notes_subunit'] = notes
389                 adr.save_payload()
390
391                 self.address = adr
392
393                 return True
394         #--------------------------------------------------------
395         # event handling
396         #--------------------------------------------------------
397         def __register_interests(self):
398                 self._PRW_zip.add_callback_on_lose_focus(self._on_zip_set)
399                 self._PRW_country.add_callback_on_lose_focus(self._on_country_set)
400         #--------------------------------------------------------
401         def _on_zip_set(self):
402                 """Set the street, town, state and country according to entered zip code."""
403                 zip_code = self._PRW_zip.GetValue()
404                 if zip_code.strip() == u'':
405                         self._PRW_street.unset_context(context = u'zip')
406                         self._PRW_urb.unset_context(context = u'zip')
407                         self._PRW_state.unset_context(context = u'zip')
408                         self._PRW_country.unset_context(context = u'zip')
409                 else:
410                         self._PRW_street.set_context(context = u'zip', val = zip_code)
411                         self._PRW_urb.set_context(context = u'zip', val = zip_code)
412                         self._PRW_state.set_context(context = u'zip', val = zip_code)
413                         self._PRW_country.set_context(context = u'zip', val = zip_code)
414         #--------------------------------------------------------
415         def _on_country_set(self):
416                 """Set the states according to entered country."""
417                 country = self._PRW_country.GetData()
418                 if country is None:
419                         self._PRW_state.unset_context(context = 'country')
420                 else:
421                         self._PRW_state.set_context(context = 'country', val = country)
422         #--------------------------------------------------------
423         # internal helpers
424         #--------------------------------------------------------
425         def __valid_for_save(self):
426
427                 required_fields = (
428                         self._PRW_type,
429                         self._PRW_zip,
430                         self._PRW_street,
431                         self._TCTRL_number,
432                         self._PRW_urb,
433                         self._PRW_state,
434                         self._PRW_country
435                 )
436                 # validate required fields
437                 is_any_field_filled = False
438                 for field in required_fields:
439                         if len(field.GetValue().strip()) > 0:
440                                 is_any_field_filled = True
441                                 field.SetBackgroundColour(wx.SystemSettings_GetColour(wx.SYS_COLOUR_WINDOW))
442                                 field.Refresh()
443                                 continue
444                         if is_any_field_filled:
445                                 field.SetBackgroundColour('pink')
446                                 field.SetFocus()
447                                 field.Refresh()
448                                 gmGuiHelpers.gm_show_error (
449                                         _('Address details must be filled in completely or not at all.'),
450                                         _('Saving contact data')
451                                 )
452                                 return False
453
454                 return True
455 #============================================================
456 class cAddressPhraseWheel(gmPhraseWheel.cPhraseWheel):
457
458         def __init__(self, *args, **kwargs):
459
460                 query = u"""
461 select * from (
462         (select
463                 pk_address,
464                 (street || ' ' || number || coalesce(' (' || subunit || ')', '') || ', '
465                 || urb || coalesce(' (' || suburb || ')', '')
466                 || coalesce(', ' || notes_street, '')
467                 || coalesce(', ' || notes_subunit, '')
468                 ) as address
469         from
470                 dem.v_address
471         where
472                 street %(fragment_condition)s
473
474         ) union (
475
476         select
477                 pk_address,
478                 (street || ' ' || number || coalesce(' (' || subunit || ')', '') || ', '
479                 || urb || coalesce(' (' || suburb || ')', '')
480                 || coalesce(', ' || notes_street, '')
481                 || coalesce(', ' || notes_subunit, '')
482                 ) as address
483         from
484                 dem.v_address
485         where
486                 postcode_street %(fragment_condition)s
487
488         ) union (
489
490         select
491                 pk_address,
492                 (street || ' ' || number || coalesce(' (' || subunit || ')', '') || ', '
493                 || urb || coalesce(' (' || suburb || ')', '')
494                 || coalesce(', ' || notes_street, '')
495                 || coalesce(', ' || notes_subunit, '')
496                 ) as address
497         from
498                 dem.v_address
499         where
500                 postcode_urb %(fragment_condition)s
501         )
502 ) as union_result
503 order by union_result.address limit 50"""
504                 mp = gmMatchProvider.cMatchProvider_SQL2(queries=query)
505                 mp.setThresholds(2, 4, 6)
506 #               mp.setWordSeparators(separators=u'[ \t]+')
507                 gmPhraseWheel.cPhraseWheel.__init__ (
508                         self,
509                         *args,
510                         **kwargs
511                 )
512                 self.matcher = mp
513                 self.SetToolTipString(_('Select an address by postcode or street name.'))
514                 self.selection_only = True
515 #============================================================
516 class cAddressTypePhraseWheel(gmPhraseWheel.cPhraseWheel):
517
518         def __init__(self, *args, **kwargs):
519
520                 query = u"""
521 select id, type from ((
522         select id, _(name) as type, 1 as rank
523         from dem.address_type
524         where _(name) %(fragment_condition)s
525 ) union (
526         select id, name as type, 2 as rank
527         from dem.address_type
528         where name %(fragment_condition)s
529 )) as ur
530 order by
531         ur.rank, ur.type
532 """
533                 mp = gmMatchProvider.cMatchProvider_SQL2(queries=query)
534                 mp.setThresholds(1, 2, 4)
535                 mp.setWordSeparators(separators=u'[ \t]+')
536                 gmPhraseWheel.cPhraseWheel.__init__ (
537                         self,
538                         *args,
539                         **kwargs
540                 )
541                 self.matcher = mp
542                 self.SetToolTipString(_('Select the type of address.'))
543 #               self.capitalisation_mode = gmTools.CAPS_FIRST
544                 self.selection_only = True
545         #--------------------------------------------------------
546 #       def GetData(self, can_create=False):
547 #               if self.data is None:
548 #                       if can_create:
549 #                               self.data = gmMedDoc.create_document_type(self.GetValue().strip())['pk_doc_type']       # FIXME: error handling
550 #               return self.data
551 #============================================================
552 class cStateSelectionPhraseWheel(gmPhraseWheel.cPhraseWheel):
553
554         def __init__(self, *args, **kwargs):
555
556                 context = {
557                         u'ctxt_country_name': {
558                                 u'where_part': u'and l10n_country ilike %(country_name)s or country ilike %(country_name)s',
559                                 u'placeholder': u'country_name'
560                         },
561                         u'ctxt_zip': {
562                                 u'where_part': u'and zip ilike %(zip)s',
563                                 u'placeholder': u'zip'
564                         },
565                         u'ctxt_country_code': {
566                                 u'where_part': u'and country in (select code from dem.country where _(name) ilike %(country_name)s or name ilike %(country_name)s)',
567                                 u'placeholder': u'country_name'
568                         }
569                 }
570
571                 query = u"""
572 select code, name from (
573         select distinct on (code, name) code, name, rank from (
574                         -- 1: find states based on name, context: zip and country name
575                         select
576                                 code_state as code, state as name, 1 as rank
577                         from dem.v_zip2data
578                         where
579                                 state %(fragment_condition)s
580                                 %(ctxt_country_name)s
581                                 %(ctxt_zip)s
582
583                 union all
584
585                         -- 2: find states based on code, context: zip and country name
586                         select
587                                 code_state as code, state as name, 2 as rank
588                         from dem.v_zip2data
589                         where
590                                 code_state %(fragment_condition)s
591                                 %(ctxt_country_name)s
592                                 %(ctxt_zip)s
593
594                 union all
595
596                         -- 3: find states based on name, context: country
597                         select
598                                 code as code, name as name, 3 as rank
599                         from dem.state
600                         where
601                                 name %(fragment_condition)s
602                                 %(ctxt_country_code)s
603
604                 union all
605
606                         -- 4: find states based on code, context: country
607                         select
608                                 code as code, name as name, 3 as rank
609                         from dem.state
610                         where
611                                 code %(fragment_condition)s
612                                 %(ctxt_country_code)s
613
614         ) as q2
615 ) as q1 order by rank, name limit 50"""
616
617                 mp = gmMatchProvider.cMatchProvider_SQL2(queries=query, context=context)
618                 mp.setThresholds(2, 5, 6)
619                 mp.setWordSeparators(separators=u'[ \t]+')
620                 gmPhraseWheel.cPhraseWheel.__init__ (
621                         self,
622                         *args,
623                         **kwargs
624                 )
625                 self.unset_context(context = u'zip')
626                 self.unset_context(context = u'country_name')
627
628                 self.matcher = mp
629                 self.SetToolTipString(_("Select a state/region/province/territory."))
630                 self.capitalisation_mode = gmTools.CAPS_FIRST
631                 self.selection_only = True
632 #============================================================
633 class cZipcodePhraseWheel(gmPhraseWheel.cPhraseWheel):
634
635         def __init__(self, *args, **kwargs):
636                 # FIXME: add possible context
637                 query = u"""
638                         (select distinct postcode, postcode from dem.street where postcode %(fragment_condition)s limit 20)
639                                 union
640                         (select distinct postcode, postcode from dem.urb where postcode %(fragment_condition)s limit 20)"""
641                 mp = gmMatchProvider.cMatchProvider_SQL2(queries=query)
642                 mp.setThresholds(2, 3, 15)
643                 gmPhraseWheel.cPhraseWheel.__init__ (
644                         self,
645                         *args,
646                         **kwargs
647                 )
648                 self.SetToolTipString(_("Type or select a zip code (postcode)."))
649                 self.matcher = mp
650 #============================================================
651 class cStreetPhraseWheel(gmPhraseWheel.cPhraseWheel):
652
653         def __init__(self, *args, **kwargs):
654                 context = {
655                         u'ctxt_zip': {
656                                 u'where_part': u'and zip ilike %(zip)s',
657                                 u'placeholder': u'zip'
658                         }
659                 }
660                 query = u"""
661 select s1, s2 from (
662         select distinct on (s1, s2) s1, s2, rank from (
663                         select
664                                 street as s1, street as s2, 1 as rank
665                         from dem.v_zip2data
666                         where
667                                 street %(fragment_condition)s
668                                 %(ctxt_zip)s
669
670                 union all
671
672                         select
673                                 name as s1, name as s2, 2 as rank
674                         from dem.street
675                         where
676                                 name %(fragment_condition)s
677
678         ) as q2
679 ) as q1 order by rank, s2 limit 50"""
680                 mp = gmMatchProvider.cMatchProvider_SQL2(queries=query, context=context)
681                 mp.setThresholds(3, 5, 8)
682                 gmPhraseWheel.cPhraseWheel.__init__ (
683                         self,
684                         *args,
685                         **kwargs
686                 )
687                 self.unset_context(context = u'zip')
688
689                 self.SetToolTipString(_('Type or select a street.'))
690                 self.capitalisation_mode = gmTools.CAPS_FIRST
691                 self.matcher = mp
692 #============================================================
693 class cSuburbPhraseWheel(gmPhraseWheel.cPhraseWheel):
694
695         def __init__(self, *args, **kwargs):
696
697                 query = """
698 select distinct on (suburb) suburb, suburb
699 from dem.street
700 where suburb %(fragment_condition)s
701 order by suburb
702 limit 50
703 """
704                 mp = gmMatchProvider.cMatchProvider_SQL2(queries=query)
705                 mp.setThresholds(2, 3, 6)
706                 gmPhraseWheel.cPhraseWheel.__init__ (
707                         self,
708                         *args,
709                         **kwargs
710                 )
711
712                 self.SetToolTipString(_('Type or select the suburb.'))
713                 self.capitalisation_mode = gmTools.CAPS_FIRST
714                 self.matcher = mp
715 #============================================================
716 class cUrbPhraseWheel(gmPhraseWheel.cPhraseWheel):
717
718         def __init__(self, *args, **kwargs):
719                 context = {
720                         u'ctxt_zip': {
721                                 u'where_part': u'and zip ilike %(zip)s',
722                                 u'placeholder': u'zip'
723                         }
724                 }
725                 query = u"""
726 select u1, u2 from (
727         select distinct on (u1,u2) u1, u2, rank from (
728                         select
729                                 urb as u1, urb as u2, 1 as rank
730                         from dem.v_zip2data
731                         where
732                                 urb %(fragment_condition)s
733                                 %(ctxt_zip)s
734
735                 union all
736
737                         select
738                                 name as u1, name as u2, 2 as rank
739                         from dem.urb
740                         where
741                                 name %(fragment_condition)s
742
743         ) as q2
744 ) as q1 order by rank, u2 limit 50"""
745                 mp = gmMatchProvider.cMatchProvider_SQL2(queries=query, context=context)
746                 mp.setThresholds(3, 5, 7)
747                 gmPhraseWheel.cPhraseWheel.__init__ (
748                         self,
749                         *args,
750                         **kwargs
751                 )
752                 self.unset_context(context = u'zip')
753
754                 self.SetToolTipString(_('Type or select a city/town/village/dwelling.'))
755                 self.capitalisation_mode = gmTools.CAPS_FIRST
756                 self.matcher = mp
757 #============================================================
758 class cCountryPhraseWheel(gmPhraseWheel.cPhraseWheel):
759
760         # FIXME: default in config
761
762         def __init__(self, *args, **kwargs):
763                 context = {
764                         u'ctxt_zip': {
765                                 u'where_part': u'and zip ilike %(zip)s',
766                                 u'placeholder': u'zip'
767                         }
768                 }
769                 query = u"""
770 select code, name from (
771         select distinct on (code, name) code, name, rank from (
772
773                 -- localized to user
774
775                         select
776                                 code_country as code, l10n_country as name, 1 as rank
777                         from dem.v_zip2data
778                         where
779                                 l10n_country %(fragment_condition)s
780                                 %(ctxt_zip)s
781
782                 union all
783
784                         select
785                                 code as code, _(name) as name, 2 as rank
786                         from dem.country
787                         where
788                                 _(name) %(fragment_condition)s
789
790                 union all
791
792                 -- non-localized
793
794                         select
795                                 code_country as code, country as name, 3 as rank
796                         from dem.v_zip2data
797                         where
798                                 country %(fragment_condition)s
799                                 %(ctxt_zip)s
800
801                 union all
802
803                         select
804                                 code as code, name as name, 4 as rank
805                         from dem.country
806                         where
807                                 name %(fragment_condition)s
808
809         ) as q2
810 ) as q1 order by rank, name limit 25"""
811                 mp = gmMatchProvider.cMatchProvider_SQL2(queries=query, context=context)
812                 mp.setThresholds(2, 5, 9)
813                 gmPhraseWheel.cPhraseWheel.__init__ (
814                         self,
815                         *args,
816                         **kwargs
817                 )
818                 self.unset_context(context = u'zip')
819
820                 self.SetToolTipString(_('Type or select a country.'))
821                 self.capitalisation_mode = gmTools.CAPS_FIRST
822                 self.selection_only = True
823                 self.matcher = mp
824 #============================================================
825 # communications channel related widgets
826 #============================================================
827 class cCommChannelTypePhraseWheel(gmPhraseWheel.cPhraseWheel):
828
829         def __init__(self, *args, **kwargs):
830
831                 query = u"""
832 select pk, type from ((
833         select pk, _(description) as type, 1 as rank
834         from dem.enum_comm_types
835         where _(description) %(fragment_condition)s
836 ) union (
837         select pk, description as type, 2 as rank
838         from dem.enum_comm_types
839         where description %(fragment_condition)s
840 )) as ur
841 order by
842         ur.rank, ur.type
843 """
844                 mp = gmMatchProvider.cMatchProvider_SQL2(queries=query)
845                 mp.setThresholds(1, 2, 4)
846                 mp.setWordSeparators(separators=u'[ \t]+')
847                 gmPhraseWheel.cPhraseWheel.__init__ (
848                         self,
849                         *args,
850                         **kwargs
851                 )
852                 self.matcher = mp
853                 self.SetToolTipString(_('Select the type of communications channel.'))
854                 self.selection_only = True
855 #------------------------------------------------------------
856 class cCommChannelEditAreaPnl(wxgCommChannelEditAreaPnl.wxgCommChannelEditAreaPnl):
857         """An edit area for editing/creating a comms channel.
858
859         Does NOT act on/listen to the current patient.
860         """
861         def __init__(self, *args, **kwargs):
862                 try:
863                         self.channel = kwargs['comm_channel']
864                         del kwargs['comm_channel']
865                 except KeyError:
866                         self.channel = None
867
868                 wxgCommChannelEditAreaPnl.wxgCommChannelEditAreaPnl.__init__(self, *args, **kwargs)
869
870                 self.identity = None
871
872                 self.refresh()
873         #--------------------------------------------------------
874         # external API
875         #--------------------------------------------------------
876         def refresh(self, comm_channel = None):
877                 if comm_channel is not None:
878                         self.channel = comm_channel
879
880                 if self.channel is not None:
881                         self._PRW_type.SetText(self.channel['l10n_comm_type'])
882                         self._TCTRL_url.SetValue(self.channel['url'])
883                         self._PRW_address.SetData(data = self.channel['pk_address'])
884                         self._CHBOX_confidential.SetValue(self.channel['is_confidential'])
885                 # FIXME: clear fields
886 #               else:
887 #                       pass
888         #--------------------------------------------------------
889         def save(self):
890                 """Links comm channel to patient."""
891
892                 if not self.__valid_for_save():
893                         return False
894
895                 self.identity.link_comm_channel (
896                         pk_channel_type = self._PRW_type.GetData(),
897                         url = self._TCTRL_url.GetValue().strip(),
898                         is_confidential = self._CHBOX_confidential.GetValue(),
899                         pk_address = self._PRW_address.GetData()
900                 )
901
902                 return True
903         #--------------------------------------------------------
904         # internal helpers
905         #--------------------------------------------------------
906         def __valid_for_save(self):
907
908                 no_errors = True
909
910                 if self._PRW_type.GetData() is None:
911                         self._PRW_type.SetBackgroundColour('pink')
912                         self._PRW_type.SetFocus()
913                         self._PRW_type.Refresh()
914                         no_errors = False
915                 else:
916                         self._PRW_type.SetBackgroundColour(wx.SystemSettings_GetColour(wx.SYS_COLOUR_WINDOW))
917                         self._PRW_type.Refresh()
918
919                 if self._TCTRL_url.GetValue().strip() == u'':
920                         self._TCTRL_url.SetBackgroundColour('pink')
921                         self._TCTRL_url.SetFocus()
922                         self._TCTRL_url.Refresh()
923                         no_errors = False
924                 else:
925                         self._TCTRL_url.SetBackgroundColour(wx.SystemSettings_GetColour(wx.SYS_COLOUR_WINDOW))
926                         self._TCTRL_url.Refresh()
927
928                 return no_errors
929 #------------------------------------------------------------
930 class cPersonCommsManagerPnl(gmListWidgets.cGenericListManagerPnl):
931         """A list for managing a person's comm channels.
932
933         Does NOT act on/listen to the current patient.
934         """
935         def __init__(self, *args, **kwargs):
936
937                 try:
938                         self.__identity = kwargs['identity']
939                         del kwargs['identity']
940                 except KeyError:
941                         self.__identity = None
942
943                 gmListWidgets.cGenericListManagerPnl.__init__(self, *args, **kwargs)
944
945                 self.new_callback = self._add_comm
946 #               self.edit_callback = self._edit_comm
947                 self.delete_callback = self._del_comm
948                 self.refresh_callback = self.refresh
949
950                 self.__init_ui()
951                 self.refresh()
952         #--------------------------------------------------------
953         # external API
954         #--------------------------------------------------------
955         def refresh(self, *args, **kwargs):
956                 if self.__identity is None:
957                         self._LCTRL_items.set_string_items()
958                         return
959
960                 comms = self.__identity.get_comm_channels()
961                 self._LCTRL_items.set_string_items (
962                         items = [ [ gmTools.bool2str(c['is_confidential'], u'X', u''), c['l10n_comm_type'], c['url'] ] for c in comms ]
963                 )
964                 self._LCTRL_items.set_column_widths()
965                 self._LCTRL_items.set_data(data = comms)
966         #--------------------------------------------------------
967         # internal helpers
968         #--------------------------------------------------------
969         def __init_ui(self):
970                 self._LCTRL_items.set_columns(columns = [
971                         _('confidential'),
972                         _('Type'),
973                         _('URL')
974                 ])
975         #--------------------------------------------------------
976         def _add_comm(self):
977                 ea = cCommChannelEditAreaPnl(self, -1)
978                 ea.identity = self.__identity
979                 dlg = gmEditArea.cGenericEditAreaDlg(self, -1, edit_area = ea)
980                 dlg.SetTitle(_('Adding new communications channel'))
981                 if dlg.ShowModal() == wx.ID_OK:
982                         return True
983                 return False
984         #--------------------------------------------------------
985         def _edit_comm(self, comm_channel):
986                 ea = cCommChannelEditAreaPnl(self, -1, comm_channel = comm_channel)
987                 ea.identity = self.__identity
988                 dlg = gmEditArea.cGenericEditAreaDlg(self, -1, edit_area = ea)
989                 dlg.SetTitle(_('Editing communications channel'))
990                 if dlg.ShowModal() == wx.ID_OK:
991                         return True
992                 return False
993         #--------------------------------------------------------
994         def _del_comm(self, comm):
995                 go_ahead = gmGuiHelpers.gm_show_question (
996                         _(      'Are you sure this patient can no longer\n'
997                                 "be contacted via this channel ?"
998                         ),
999                         _('Removing communication channel')
1000                 )
1001                 if not go_ahead:
1002                         return False
1003                 self.__identity.unlink_comm_channel(comm_channel = comm)
1004                 return True
1005         #--------------------------------------------------------
1006         # properties
1007         #--------------------------------------------------------
1008         def _get_identity(self):
1009                 return self.__identity
1010
1011         def _set_identity(self, identity):
1012                 self.__identity = identity
1013                 self.refresh()
1014
1015         identity = property(_get_identity, _set_identity)
1016 #============================================================
1017 # identity widgets
1018 #============================================================
1019 # phrasewheels
1020 #------------------------------------------------------------
1021 class cLastnamePhraseWheel(gmPhraseWheel.cPhraseWheel):
1022
1023         def __init__(self, *args, **kwargs):
1024                 query = u"select distinct lastnames, lastnames from dem.names where lastnames %(fragment_condition)s order by lastnames limit 25"
1025                 mp = gmMatchProvider.cMatchProvider_SQL2(queries=query)
1026                 mp.setThresholds(3, 5, 9)
1027                 gmPhraseWheel.cPhraseWheel.__init__ (
1028                         self,
1029                         *args,
1030                         **kwargs
1031                 )
1032                 self.SetToolTipString(_("Type or select a last name (family name/surname)."))
1033                 self.capitalisation_mode = gmTools.CAPS_NAMES
1034                 self.matcher = mp
1035 #------------------------------------------------------------
1036 class cFirstnamePhraseWheel(gmPhraseWheel.cPhraseWheel):
1037
1038         def __init__(self, *args, **kwargs):
1039                 query = u"""
1040                         (select distinct firstnames, firstnames from dem.names where firstnames %(fragment_condition)s order by firstnames limit 20)
1041                                 union
1042                         (select distinct name, name from dem.name_gender_map where name %(fragment_condition)s order by name limit 20)"""
1043                 mp = gmMatchProvider.cMatchProvider_SQL2(queries=query)
1044                 mp.setThresholds(3, 5, 9)
1045                 gmPhraseWheel.cPhraseWheel.__init__ (
1046                         self,
1047                         *args,
1048                         **kwargs
1049                 )
1050                 self.SetToolTipString(_("Type or select a first name (forename/Christian name/given name)."))
1051                 self.capitalisation_mode = gmTools.CAPS_NAMES
1052                 self.matcher = mp
1053 #------------------------------------------------------------
1054 class cNicknamePhraseWheel(gmPhraseWheel.cPhraseWheel):
1055
1056         def __init__(self, *args, **kwargs):
1057                 query = u"""
1058                         (select distinct preferred, preferred from dem.names where preferred %(fragment_condition)s order by preferred limit 20)
1059                                 union
1060                         (select distinct firstnames, firstnames from dem.names where firstnames %(fragment_condition)s order by firstnames limit 20)
1061                                 union
1062                         (select distinct name, name from dem.name_gender_map where name %(fragment_condition)s order by name limit 20)"""
1063                 mp = gmMatchProvider.cMatchProvider_SQL2(queries=query)
1064                 mp.setThresholds(3, 5, 9)
1065                 gmPhraseWheel.cPhraseWheel.__init__ (
1066                         self,
1067                         *args,
1068                         **kwargs
1069                 )
1070                 self.SetToolTipString(_("Type or select an alias (nick name, preferred name, call name, warrior name, artist name)."))
1071                 # nicknames CAN start with lower case !
1072                 #self.capitalisation_mode = gmTools.CAPS_NAMES
1073                 self.matcher = mp
1074 #------------------------------------------------------------
1075 class cTitlePhraseWheel(gmPhraseWheel.cPhraseWheel):
1076
1077         def __init__(self, *args, **kwargs):
1078                 query = u"select distinct title, title from dem.identity where title %(fragment_condition)s"
1079                 mp = gmMatchProvider.cMatchProvider_SQL2(queries=query)
1080                 mp.setThresholds(1, 3, 9)
1081                 gmPhraseWheel.cPhraseWheel.__init__ (
1082                         self,
1083                         *args,
1084                         **kwargs
1085                 )
1086                 self.SetToolTipString(_("Type or select a title. Note that the title applies to the person, not to a particular name !"))
1087                 self.matcher = mp
1088 #------------------------------------------------------------
1089 class cGenderSelectionPhraseWheel(gmPhraseWheel.cPhraseWheel):
1090         """Let user select a gender."""
1091
1092         _gender_map = None
1093
1094         def __init__(self, *args, **kwargs):
1095
1096                 if cGenderSelectionPhraseWheel._gender_map is None:
1097                         cmd = u"""
1098                                 select tag, l10n_label, sort_weight
1099                                 from dem.v_gender_labels
1100                                 order by sort_weight desc"""
1101                         rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd}], get_col_idx=True)
1102                         cGenderSelectionPhraseWheel._gender_map = {}
1103                         for gender in rows:
1104                                 cGenderSelectionPhraseWheel._gender_map[gender[idx['tag']]] = {
1105                                         'data': gender[idx['tag']],
1106                                         'label': gender[idx['l10n_label']],
1107                                         'weight': gender[idx['sort_weight']]
1108                                 }
1109
1110                 mp = gmMatchProvider.cMatchProvider_FixedList(aSeq = cGenderSelectionPhraseWheel._gender_map.values())
1111                 mp.setThresholds(1, 1, 3)
1112
1113                 gmPhraseWheel.cPhraseWheel.__init__(self, *args, **kwargs)
1114                 self.selection_only = True
1115                 self.matcher = mp
1116                 self.picklist_delay = 50
1117 #------------------------------------------------------------
1118 class cOccupationPhraseWheel(gmPhraseWheel.cPhraseWheel):
1119
1120         def __init__(self, *args, **kwargs):
1121                 query = u"select distinct name, _(name) from dem.occupation where _(name) %(fragment_condition)s"
1122                 mp = gmMatchProvider.cMatchProvider_SQL2(queries=query)
1123                 mp.setThresholds(1, 3, 5)
1124                 gmPhraseWheel.cPhraseWheel.__init__ (
1125                         self,
1126                         *args,
1127                         **kwargs
1128                 )
1129                 self.SetToolTipString(_("Type or select an occupation."))
1130                 self.capitalisation_mode = gmTools.CAPS_FIRST
1131                 self.matcher = mp
1132 #------------------------------------------------------------
1133 class cExternalIDTypePhraseWheel(gmPhraseWheel.cPhraseWheel):
1134
1135         def __init__(self, *args, **kwargs):
1136                 query = u"""
1137 select distinct pk, (name || coalesce(' (%s ' || issuer || ')', '')) as label
1138 from dem.enum_ext_id_types
1139 where name %%(fragment_condition)s
1140 order by label limit 25""" % _('issued by')
1141                 mp = gmMatchProvider.cMatchProvider_SQL2(queries=query)
1142                 mp.setThresholds(1, 3, 5)
1143                 gmPhraseWheel.cPhraseWheel.__init__(self, *args, **kwargs)
1144                 self.SetToolTipString(_("Enter or select a type for the external ID."))
1145                 self.matcher = mp
1146 #------------------------------------------------------------
1147 class cExternalIDIssuerPhraseWheel(gmPhraseWheel.cPhraseWheel):
1148
1149         def __init__(self, *args, **kwargs):
1150                 query = u"""
1151 select distinct issuer, issuer
1152 from dem.enum_ext_id_types
1153 where issuer %(fragment_condition)s
1154 order by issuer limit 25"""
1155                 mp = gmMatchProvider.cMatchProvider_SQL2(queries=query)
1156                 mp.setThresholds(1, 3, 5)
1157                 gmPhraseWheel.cPhraseWheel.__init__(self, *args, **kwargs)
1158                 self.SetToolTipString(_("Type or select an occupation."))
1159                 self.capitalisation_mode = gmTools.CAPS_FIRST
1160                 self.matcher = mp
1161 #------------------------------------------------------------
1162 # edit areas
1163 #------------------------------------------------------------
1164 class cExternalIDEditAreaPnl(wxgExternalIDEditAreaPnl.wxgExternalIDEditAreaPnl):
1165         """An edit area for editing/creating external IDs.
1166
1167         Does NOT act on/listen to the current patient.
1168         """
1169         def __init__(self, *args, **kwargs):
1170         
1171                 try:
1172                         self.ext_id = kwargs['external_id']
1173                         del kwargs['external_id']
1174                 except:
1175                         self.ext_id = None
1176
1177                 wxgExternalIDEditAreaPnl.wxgExternalIDEditAreaPnl.__init__(self, *args, **kwargs)
1178
1179                 self.identity = None
1180
1181                 self.__register_events()
1182
1183                 self.refresh()
1184         #--------------------------------------------------------
1185         # external API
1186         #--------------------------------------------------------
1187         def refresh(self, ext_id=None):
1188                 if ext_id is not None:
1189                         self.ext_id = ext_id
1190
1191                 if self.ext_id is not None:
1192                         self._PRW_type.SetText(value = self.ext_id['name'], data = self.ext_id['pk_type'])
1193                         self._TCTRL_value.SetValue(self.ext_id['value'])
1194                         self._PRW_issuer.SetText(self.ext_id['issuer'])
1195                         self._TCTRL_comment.SetValue(gmTools.coalesce(self.ext_id['comment'], u''))
1196                 # FIXME: clear fields
1197 #               else:
1198 #                       pass
1199         #--------------------------------------------------------
1200         def save(self):
1201
1202                 if not self.__valid_for_save():
1203                         return False
1204
1205                 # strip out " (issued by ...)" added by phrasewheel
1206                 type = regex.split(' \(%s .+\)$' % _('issued by'), self._PRW_type.GetValue().strip(), 1)[0]
1207
1208                 # add new external ID
1209                 if self.ext_id is None:
1210                         self.identity.add_external_id (
1211                                 id_type = type,
1212                                 id_value = self._TCTRL_value.GetValue().strip(),
1213                                 issuer = gmTools.none_if(self._PRW_issuer.GetValue().strip(), u''),
1214                                 comment = gmTools.none_if(self._TCTRL_comment.GetValue().strip(), u'')
1215                         )
1216                 # edit old external ID
1217                 else:
1218                         self.identity.update_external_id (
1219                                 pk_id = self.ext_id['pk_id'],
1220                                 type = type,
1221                                 value = self._TCTRL_value.GetValue().strip(),
1222                                 issuer = gmTools.none_if(self._PRW_issuer.GetValue().strip(), u''),
1223                                 comment = gmTools.none_if(self._TCTRL_comment.GetValue().strip(), u'')
1224                         )
1225
1226                 return True
1227         #--------------------------------------------------------
1228         # internal helpers
1229         #--------------------------------------------------------
1230         def __register_events(self):
1231                 self._PRW_type.add_callback_on_lose_focus(self._on_type_set)
1232         #--------------------------------------------------------
1233         def _on_type_set(self):
1234                 """Set the issuer according to the selected type.
1235
1236                 Matches are fetched from existing records in backend.
1237                 """
1238                 pk_curr_type = self._PRW_type.GetData()
1239                 if pk_curr_type is None:
1240                         return True
1241                 rows, idx = gmPG2.run_ro_queries(queries = [{
1242                         'cmd': u"select issuer from dem.enum_ext_id_types where pk = %s",
1243                         'args': [pk_curr_type]
1244                 }])
1245                 if len(rows) == 0:
1246                         return True
1247                 wx.CallAfter(self._PRW_issuer.SetText, rows[0][0])
1248                 return True
1249         #--------------------------------------------------------
1250         def __valid_for_save(self):
1251
1252                 no_errors = True
1253
1254                 if self._PRW_type.GetData() is None:
1255                         self._PRW_type.SetBackgroundColour('pink')
1256                         self._PRW_type.SetFocus()
1257                         self._PRW_type.Refresh()
1258                         no_errors = False
1259                 else:
1260                         self._PRW_type.SetBackgroundColour(wx.SystemSettings_GetColour(wx.SYS_COLOUR_WINDOW))
1261                         self._PRW_type.Refresh()
1262
1263                 if self._TCTRL_value.GetValue().strip() == u'':
1264                         self._TCTRL_value.SetBackgroundColour('pink')
1265                         self._TCTRL_value.SetFocus()
1266                         self._TCTRL_value.Refresh()
1267                         no_errors = False
1268                 else:
1269                         self._TCTRL_value.SetBackgroundColour(wx.SystemSettings_GetColour(wx.SYS_COLOUR_WINDOW))
1270                         self._TCTRL_value.Refresh()
1271
1272                 return no_errors
1273 #------------------------------------------------------------
1274 class cNameGenderDOBEditAreaPnl(wxgNameGenderDOBEditAreaPnl.wxgNameGenderDOBEditAreaPnl):
1275         """An edit area for editing/creating name/gender/dob.
1276
1277         Does NOT act on/listen to the current patient.
1278         """
1279         def __init__(self, *args, **kwargs):
1280
1281                 self.__name = kwargs['name']
1282                 del kwargs['name']
1283                 self.__identity = gmPerson.cIdentity(aPK_obj = self.__name['pk_identity'])
1284
1285                 wxgNameGenderDOBEditAreaPnl.wxgNameGenderDOBEditAreaPnl.__init__(self, *args, **kwargs)
1286
1287                 self.__register_interests()
1288                 self.refresh()
1289         #--------------------------------------------------------
1290         # external API
1291         #--------------------------------------------------------
1292         def refresh(self):
1293                 if self.__name is None:
1294                         return
1295
1296                 self._PRW_title.SetText(gmTools.coalesce(self.__name['title'], u''))
1297                 self._PRW_firstname.SetText(self.__name['firstnames'])
1298                 self._PRW_lastname.SetText(self.__name['lastnames'])
1299                 self._PRW_nick.SetText(gmTools.coalesce(self.__name['preferred'], u''))
1300                 dob = self.__identity['dob']
1301                 self._PRW_dob.SetText(value = dob.strftime('%Y-%m-%d %H:%M'), data = dob)
1302                 self._PRW_gender.SetData(self.__name['gender'])
1303                 self._CHBOX_active.SetValue(self.__name['active_name'])
1304                 self._TCTRL_comment.SetValue(gmTools.coalesce(self.__name['comment'], u''))
1305                 # FIXME: clear fields
1306 #               else:
1307 #                       pass
1308         #--------------------------------------------------------
1309         def save(self):
1310
1311                 if not self.__valid_for_save():
1312                         return False
1313
1314                 self.__identity['gender'] = self._PRW_gender.GetData()
1315                 self.__identity['dob'] = self._PRW_dob.GetData().get_pydt()
1316                 self.__identity['title'] = gmTools.none_if(self._PRW_title.GetValue().strip(), u'')
1317                 self.__identity.save_payload()
1318
1319                 active = self._CHBOX_active.GetValue()
1320                 first = self._PRW_firstname.GetValue().strip()
1321                 last = self._PRW_lastname.GetValue().strip()
1322                 old_nick = self.__name['preferred']
1323
1324                 # is it a new name ?
1325                 old_name = self.__name['firstnames'] + self.__name['lastnames']
1326                 if (first + last) != old_name:
1327                         self.__name = self.__identity.add_name(first, last, active)
1328
1329                 self.__name['active_name'] = active
1330                 self.__name['preferred'] = gmTools.none_if(self._PRW_nick.GetValue().strip(), u'')
1331                 self.__name['comment'] = gmTools.none_if(self._TCTRL_comment.GetValue().strip(), u'')
1332
1333                 self.__name.save_payload()
1334
1335                 return True
1336         #--------------------------------------------------------
1337         # event handling
1338         #--------------------------------------------------------
1339         def __register_interests(self):
1340                 self._PRW_firstname.add_callback_on_lose_focus(self._on_name_set)
1341         #--------------------------------------------------------
1342         def _on_name_set(self):
1343                 """Set the gender according to entered firstname.
1344
1345                 Matches are fetched from existing records in backend.
1346                 """
1347                 firstname = self._PRW_firstname.GetValue().strip()
1348                 if firstname == u'':
1349                         return True
1350                 rows, idx = gmPG2.run_ro_queries(queries = [{
1351                         'cmd': u"select gender from dem.name_gender_map where name ilike %s",
1352                         'args': [firstname]
1353                 }])
1354                 if len(rows) == 0:
1355                         return True
1356                 wx.CallAfter(self._PRW_gender.SetData, rows[0][0])
1357                 return True
1358         #--------------------------------------------------------
1359         # internal helpers
1360         #--------------------------------------------------------
1361         def __valid_for_save(self):
1362
1363                 error_found = True
1364
1365                 if self._PRW_gender.GetData() is None:
1366                         self._PRW_gender.SetBackgroundColour('pink')
1367                         self._PRW_gender.Refresh()
1368                         self._PRW_gender.SetFocus()
1369                         error_found = False
1370                 else:
1371                         self._PRW_gender.SetBackgroundColour(wx.SystemSettings_GetColour(wx.SYS_COLOUR_WINDOW))
1372                         self._PRW_gender.Refresh()
1373
1374                 if not self._PRW_dob.is_valid_timestamp():
1375                         val = self._PRW_dob.GetValue().strip()
1376                         gmDispatcher.send(signal = u'statustext', msg = _('Cannot parse <%s> into proper timestamp.') % val)
1377                         self._PRW_dob.SetBackgroundColour('pink')
1378                         self._PRW_dob.Refresh()
1379                         self._PRW_dob.SetFocus()
1380                         error_found = False
1381                 else:
1382                         self._PRW_dob.SetBackgroundColour(wx.SystemSettings_GetColour(wx.SYS_COLOUR_WINDOW))
1383                         self._PRW_dob.Refresh()
1384
1385                 if self._PRW_lastname.GetValue().strip() == u'':
1386                         self._PRW_lastname.SetBackgroundColour('pink')
1387                         self._PRW_lastname.Refresh()
1388                         self._PRW_lastname.SetFocus()
1389                         error_found = False
1390                 else:
1391                         self._PRW_lastname.SetBackgroundColour(wx.SystemSettings_GetColour(wx.SYS_COLOUR_WINDOW))
1392                         self._PRW_lastname.Refresh()
1393
1394                 if self._PRW_firstname.GetValue().strip() == u'':
1395                         self._PRW_firstname.SetBackgroundColour('pink')
1396                         self._PRW_firstname.Refresh()
1397                         self._PRW_firstname.SetFocus()
1398                         error_found = False
1399                 else:
1400                         self._PRW_firstname.SetBackgroundColour(wx.SystemSettings_GetColour(wx.SYS_COLOUR_WINDOW))
1401                         self._PRW_firstname.Refresh()
1402
1403                 return error_found
1404 #------------------------------------------------------------
1405 # list manager
1406 #------------------------------------------------------------
1407 class cPersonNamesManagerPnl(gmListWidgets.cGenericListManagerPnl):
1408         """A list for managing a person's names.
1409
1410         Does NOT act on/listen to the current patient.
1411         """
1412         def __init__(self, *args, **kwargs):
1413
1414                 try:
1415                         self.__identity = kwargs['identity']
1416                         del kwargs['identity']
1417                 except KeyError:
1418                         self.__identity = None
1419
1420                 gmListWidgets.cGenericListManagerPnl.__init__(self, *args, **kwargs)
1421
1422                 self.new_callback = self._add_name
1423                 self.edit_callback = self._edit_name
1424                 self.delete_callback = self._del_name
1425                 self.refresh_callback = self.refresh
1426
1427                 self.__init_ui()
1428                 self.refresh()
1429         #--------------------------------------------------------
1430         # external API
1431         #--------------------------------------------------------
1432         def refresh(self, *args, **kwargs):
1433                 if self.__identity is None:
1434                         self._LCTRL_items.set_string_items()
1435                         return
1436
1437                 names = self.__identity.get_names()
1438                 self._LCTRL_items.set_string_items (
1439                         items = [ [
1440                                         gmTools.bool2str(n['active_name'], 'X', ''),
1441                                         gmTools.coalesce(n['title'], gmPerson.map_gender2salutation(n['gender'])),
1442                                         n['lastnames'],
1443                                         n['firstnames'],
1444                                         gmTools.coalesce(n['preferred'], u''),
1445                                         gmTools.coalesce(n['comment'], u'')
1446                                 ] for n in names ]
1447                 )
1448                 self._LCTRL_items.set_column_widths()
1449                 self._LCTRL_items.set_data(data = names)
1450         #--------------------------------------------------------
1451         # internal helpers
1452         #--------------------------------------------------------
1453         def __init_ui(self):
1454                 self._LCTRL_items.set_columns(columns = [
1455                         _('Active'),
1456                         _('Title'),
1457                         _('Lastname'),
1458                         _('Firstname'),
1459                         _('Preferred Name'),
1460                         _('Comment')
1461                 ])
1462         #--------------------------------------------------------
1463         def _add_name(self):
1464                 ea = cNameGenderDOBEditAreaPnl(self, -1, name = self.__identity.get_active_name())
1465                 dlg = gmEditArea.cGenericEditAreaDlg(self, -1, edit_area = ea)
1466                 dlg.SetTitle(_('Adding new name'))
1467                 if dlg.ShowModal() == wx.ID_OK:
1468                         dlg.Destroy()
1469                         return True
1470                 dlg.Destroy()
1471                 return False
1472         #--------------------------------------------------------
1473         def _edit_name(self, name):
1474                 ea = cNameGenderDOBEditAreaPnl(self, -1, name = name)
1475                 dlg = gmEditArea.cGenericEditAreaDlg(self, -1, edit_area = ea)
1476                 dlg.SetTitle(_('Editing name'))
1477                 if dlg.ShowModal() == wx.ID_OK:
1478                         dlg.Destroy()
1479                         return True
1480                 dlg.Destroy()
1481                 return False
1482         #--------------------------------------------------------
1483         def _del_name(self, name):
1484                 go_ahead = gmGuiHelpers.gm_show_question (
1485                         _(      'It is often advisable to keep old names around and\n'
1486                                 'just create a new "currently active" name.\n'
1487                                 '\n'
1488                                 'This allows finding the patient by both the old\n'
1489                                 'and the new name (think before/after marriage).\n'
1490                                 '\n'
1491                                 'Do you still want to really delete\n'
1492                                 "this name from the patient ?"
1493                         ),
1494                         _('Deleting name')
1495                 )
1496                 if not go_ahead:
1497                         return False
1498                 self.__identity.delete_name(name = name)
1499                 return True
1500         #--------------------------------------------------------
1501         # properties
1502         #--------------------------------------------------------
1503         def _get_identity(self):
1504                 return self.__identity
1505
1506         def _set_identity(self, identity):
1507                 self.__identity = identity
1508                 self.refresh()
1509
1510         identity = property(_get_identity, _set_identity)
1511 #------------------------------------------------------------
1512 class cPersonIDsManagerPnl(gmListWidgets.cGenericListManagerPnl):
1513         """A list for managing a person's external IDs.
1514
1515         Does NOT act on/listen to the current patient.
1516         """
1517         def __init__(self, *args, **kwargs):
1518
1519                 try:
1520                         self.__identity = kwargs['identity']
1521                         del kwargs['identity']
1522                 except KeyError:
1523                         self.__identity = None
1524
1525                 gmListWidgets.cGenericListManagerPnl.__init__(self, *args, **kwargs)
1526
1527                 self.new_callback = self._add_id
1528                 self.edit_callback = self._edit_id
1529                 self.delete_callback = self._del_id
1530                 self.refresh_callback = self.refresh
1531
1532                 self.__init_ui()
1533                 self.refresh()
1534         #--------------------------------------------------------
1535         # external API
1536         #--------------------------------------------------------
1537         def refresh(self, *args, **kwargs):
1538                 if self.__identity is None:
1539                         self._LCTRL_items.set_string_items()
1540                         return
1541
1542                 ids = self.__identity.get_external_ids()
1543                 self._LCTRL_items.set_string_items (
1544                         items = [ [
1545                                         i['name'],
1546                                         i['value'],
1547                                         gmTools.coalesce(i['issuer'], u''),
1548                                         i['context'],
1549                                         gmTools.coalesce(i['comment'], u'')
1550                                 ] for i in ids
1551                         ]
1552                 )
1553                 self._LCTRL_items.set_column_widths()
1554                 self._LCTRL_items.set_data(data = ids)
1555         #--------------------------------------------------------
1556         # internal helpers
1557         #--------------------------------------------------------
1558         def __init_ui(self):
1559                 self._LCTRL_items.set_columns(columns = [
1560                         _('ID type'),
1561                         _('Value'),
1562                         _('Issuer'),
1563                         _('Context'),
1564                         _('Comment')
1565                 ])
1566         #--------------------------------------------------------
1567         def _add_id(self):
1568                 ea = cExternalIDEditAreaPnl(self, -1)
1569                 ea.identity = self.__identity
1570                 dlg = gmEditArea.cGenericEditAreaDlg(self, -1, edit_area = ea)
1571                 dlg.SetTitle(_('Adding new external ID'))
1572                 if dlg.ShowModal() == wx.ID_OK:
1573                         dlg.Destroy()
1574                         return True
1575                 dlg.Destroy()
1576                 return False
1577         #--------------------------------------------------------
1578         def _edit_id(self, ext_id):
1579                 ea = cExternalIDEditAreaPnl(self, -1, external_id = ext_id)
1580                 ea.identity = self.__identity
1581                 dlg = gmEditArea.cGenericEditAreaDlg(self, -1, edit_area = ea)
1582                 dlg.SetTitle(_('Editing external ID'))
1583                 if dlg.ShowModal() == wx.ID_OK:
1584                         dlg.Destroy()
1585                         return True
1586                 dlg.Destroy()
1587                 return False
1588         #--------------------------------------------------------
1589         def _del_id(self, ext_id):
1590                 go_ahead = gmGuiHelpers.gm_show_question (
1591                         _(      'Do you really want to delete this\n'
1592                                 'external ID from the patient ?'),
1593                         _('Deleting external ID')
1594                 )
1595                 if not go_ahead:
1596                         return False
1597                 self.__identity.delete_external_id(pk_ext_id = ext_id['pk_id'])
1598                 return True
1599         #--------------------------------------------------------
1600         # properties
1601         #--------------------------------------------------------
1602         def _get_identity(self):
1603                 return self.__identity
1604
1605         def _set_identity(self, identity):
1606                 self.__identity = identity
1607                 self.refresh()
1608
1609         identity = property(_get_identity, _set_identity)
1610 #------------------------------------------------------------
1611 # integrated panels
1612 #------------------------------------------------------------
1613 class cPersonIdentityManagerPnl(wxgPersonIdentityManagerPnl.wxgPersonIdentityManagerPnl):
1614         """A panel for editing identity data for a person.
1615
1616         - provides access to:
1617           - name
1618           - external IDs
1619
1620         Does NOT act on/listen to the current patient.
1621         """
1622         def __init__(self, *args, **kwargs):
1623
1624                 wxgPersonIdentityManagerPnl.wxgPersonIdentityManagerPnl.__init__(self, *args, **kwargs)
1625
1626                 self.__identity = None
1627                 self.refresh()
1628         #--------------------------------------------------------
1629         # external API
1630         #--------------------------------------------------------
1631         def refresh(self):
1632                 self._PNL_names.identity = self.__identity
1633                 self._PNL_ids.identity = self.__identity
1634         #--------------------------------------------------------
1635         # properties
1636         #--------------------------------------------------------
1637         def _get_identity(self):
1638                 return self.__identity
1639
1640         def _set_identity(self, identity):
1641                 self.__identity = identity
1642                 self.refresh()
1643
1644         identity = property(_get_identity, _set_identity)
1645 #============================================================
1646 # new-patient wizard classes
1647 #============================================================
1648 class cBasicPatDetailsPage(wx.wizard.WizardPageSimple):
1649         """
1650         Wizard page for entering patient's basic demographic information
1651         """
1652         
1653         form_fields = (
1654                         'firstnames', 'lastnames', 'nick', 'dob', 'gender', 'title', 'occupation',
1655                         'address_number', 'zip_code', 'street', 'town', 'state', 'country', 'phone'
1656         )
1657         
1658         def __init__(self, parent, title):
1659                 """
1660                 Creates a new instance of BasicPatDetailsPage
1661                 @param parent - The parent widget
1662                 @type parent - A wx.Window instance
1663                 @param tile - The title of the page
1664                 @type title - A StringType instance                             
1665                 """
1666                 wx.wizard.WizardPageSimple.__init__(self, parent) #, bitmap = gmGuiHelpers.gm_icon(_('oneperson'))
1667                 self.__title = title
1668                 self.__do_layout()
1669                 self.__register_interests()
1670         #--------------------------------------------------------
1671         def __do_layout(self):
1672                 PNL_form = wx.Panel(self, -1)
1673
1674                 # last name
1675                 STT_lastname = wx.StaticText(PNL_form, -1, _('Last name'))
1676                 STT_lastname.SetForegroundColour('red')
1677                 self.PRW_lastname = cLastnamePhraseWheel(parent = PNL_form, id = -1)
1678                 self.PRW_lastname.SetToolTipString(_('Required: lastname (family name)'))
1679
1680                 # first name
1681                 STT_firstname = wx.StaticText(PNL_form, -1, _('First name'))
1682                 STT_firstname.SetForegroundColour('red')
1683                 self.PRW_firstname = cFirstnamePhraseWheel(parent = PNL_form, id = -1)
1684                 self.PRW_firstname.SetToolTipString(_('Required: surname/given name/first name'))
1685
1686                 # nickname
1687                 STT_nick = wx.StaticText(PNL_form, -1, _('Nick name'))
1688                 self.PRW_nick = cNicknamePhraseWheel(parent = PNL_form, id = -1)
1689
1690                 # DOB
1691                 STT_dob = wx.StaticText(PNL_form, -1, _('Date of birth'))
1692                 STT_dob.SetForegroundColour('red')
1693                 self.PRW_dob = gmDateTimeInput.cFuzzyTimestampInput(parent = PNL_form, id = -1)
1694                 self.PRW_dob.SetToolTipString(_("Required: date of birth, if unknown or aliasing wanted then invent one"))
1695
1696                 # gender
1697                 STT_gender = wx.StaticText(PNL_form, -1, _('Gender'))
1698                 STT_gender.SetForegroundColour('red')
1699                 self.PRW_gender = cGenderSelectionPhraseWheel(parent = PNL_form, id=-1)
1700                 self.PRW_gender.SetToolTipString(_("Required: gender of patient"))
1701
1702                 # title
1703                 STT_title = wx.StaticText(PNL_form, -1, _('Title'))
1704                 self.PRW_title = cTitlePhraseWheel(parent = PNL_form, id = -1)
1705
1706                 # zip code
1707                 STT_zip_code = wx.StaticText(PNL_form, -1, _('Zip code'))
1708                 self.PRW_zip_code = cZipcodePhraseWheel(parent = PNL_form, id = -1)
1709                 self.PRW_zip_code.SetToolTipString(_("primary/home address: zip code/postcode"))
1710
1711                 # street
1712                 STT_street = wx.StaticText(PNL_form, -1, _('Street'))
1713                 self.PRW_street = cStreetPhraseWheel(parent = PNL_form, id = -1)
1714                 self.PRW_street.SetToolTipString(_("primary/home address: name of street"))
1715
1716                 # address number
1717                 STT_address_number = wx.StaticText(PNL_form, -1, _('Number'))
1718                 self.TTC_address_number = wx.TextCtrl(PNL_form, -1)
1719                 self.TTC_address_number.SetToolTipString(_("primary/home address: address number"))
1720
1721                 # town
1722                 STT_town = wx.StaticText(PNL_form, -1, _('Town'))
1723                 self.PRW_town = cUrbPhraseWheel(parent = PNL_form, id = -1)
1724                 self.PRW_town.SetToolTipString(_("primary/home address: town/village/dwelling/city/etc."))
1725
1726                 # state
1727                 STT_state = wx.StaticText(PNL_form, -1, _('State'))
1728                 self.PRW_state = cStateSelectionPhraseWheel(parent=PNL_form, id=-1)
1729                 self.PRW_state.SetToolTipString(_("primary/home address: state"))
1730
1731                 # country
1732                 STT_country = wx.StaticText(PNL_form, -1, _('Country'))
1733                 self.PRW_country = cCountryPhraseWheel(parent = PNL_form, id = -1)
1734                 self.PRW_country.SetToolTipString(_("primary/home address: country"))
1735
1736                 # phone
1737                 STT_phone = wx.StaticText(PNL_form, -1, _('Phone'))
1738                 self.TTC_phone = wx.TextCtrl(PNL_form, -1)
1739                 self.TTC_phone.SetToolTipString(_("phone number at home"))
1740
1741                 # occupation
1742                 STT_occupation = wx.StaticText(PNL_form, -1, _('Occupation'))
1743                 self.PRW_occupation = cOccupationPhraseWheel(parent = PNL_form, id = -1)
1744
1745                 # form main validator
1746                 self.form_DTD = cFormDTD(fields = self.__class__.form_fields)
1747                 PNL_form.SetValidator(cBasicPatDetailsPageValidator(dtd = self.form_DTD))
1748                                 
1749                 # layout input widgets
1750                 SZR_input = wx.FlexGridSizer(cols = 2, rows = 15, vgap = 4, hgap = 4)
1751                 SZR_input.AddGrowableCol(1)
1752                 SZR_input.Add(STT_lastname, 0, wx.SHAPED)
1753                 SZR_input.Add(self.PRW_lastname, 1, wx.EXPAND)
1754                 SZR_input.Add(STT_firstname, 0, wx.SHAPED)
1755                 SZR_input.Add(self.PRW_firstname, 1, wx.EXPAND)
1756                 SZR_input.Add(STT_nick, 0, wx.SHAPED)
1757                 SZR_input.Add(self.PRW_nick, 1, wx.EXPAND)
1758                 SZR_input.Add(STT_dob, 0, wx.SHAPED)
1759                 SZR_input.Add(self.PRW_dob, 1, wx.EXPAND)
1760                 SZR_input.Add(STT_gender, 0, wx.SHAPED)
1761                 SZR_input.Add(self.PRW_gender, 1, wx.EXPAND)
1762                 SZR_input.Add(STT_title, 0, wx.SHAPED)
1763                 SZR_input.Add(self.PRW_title, 1, wx.EXPAND)
1764                 SZR_input.Add(STT_zip_code, 0, wx.SHAPED)
1765                 SZR_input.Add(self.PRW_zip_code, 1, wx.EXPAND)
1766                 SZR_input.Add(STT_street, 0, wx.SHAPED)
1767                 SZR_input.Add(self.PRW_street, 1, wx.EXPAND)
1768                 SZR_input.Add(STT_address_number, 0, wx.SHAPED)
1769                 SZR_input.Add(self.TTC_address_number, 1, wx.EXPAND)
1770                 SZR_input.Add(STT_town, 0, wx.SHAPED)
1771                 SZR_input.Add(self.PRW_town, 1, wx.EXPAND)
1772                 SZR_input.Add(STT_state, 0, wx.SHAPED)
1773                 SZR_input.Add(self.PRW_state, 1, wx.EXPAND)
1774                 SZR_input.Add(STT_country, 0, wx.SHAPED)
1775                 SZR_input.Add(self.PRW_country, 1, wx.EXPAND)
1776                 SZR_input.Add(STT_phone, 0, wx.SHAPED)
1777                 SZR_input.Add(self.TTC_phone, 1, wx.EXPAND)
1778                 SZR_input.Add(STT_occupation, 0, wx.SHAPED)
1779                 SZR_input.Add(self.PRW_occupation, 1, wx.EXPAND)
1780
1781                 PNL_form.SetSizerAndFit(SZR_input)
1782
1783                 # layout page
1784                 SZR_main = gmGuiHelpers.makePageTitle(self, self.__title)
1785                 SZR_main.Add(PNL_form, 1, wx.EXPAND)
1786         #--------------------------------------------------------
1787         # event handling
1788         #--------------------------------------------------------
1789         def __register_interests(self):
1790                 self.PRW_firstname.add_callback_on_lose_focus(self.on_name_set)
1791                 self.PRW_country.add_callback_on_selection(self.on_country_selected)
1792                 self.PRW_zip_code.add_callback_on_lose_focus(self.on_zip_set)
1793         #--------------------------------------------------------
1794         def on_country_selected(self, data):
1795                 """Set the states according to entered country."""
1796                 self.PRW_state.set_context(context=u'country', val=data)
1797                 return True
1798         #--------------------------------------------------------
1799         def on_name_set(self):
1800                 """Set the gender according to entered firstname.
1801
1802                 Matches are fetched from existing records in backend.
1803                 """
1804                 firstname = self.PRW_firstname.GetValue().strip()
1805                 rows, idx = gmPG2.run_ro_queries(queries = [{
1806                         'cmd': u"select gender from dem.name_gender_map where name ilike %s",
1807                         'args': [firstname]
1808                 }])
1809                 if len(rows) == 0:
1810                         return True
1811                 wx.CallAfter(self.PRW_gender.SetData, rows[0][0])
1812                 return True
1813         #--------------------------------------------------------
1814         def on_zip_set(self):
1815                 """Set the street, town, state and country according to entered zip code."""
1816                 zip_code = self.PRW_zip_code.GetValue().strip()
1817                 self.PRW_street.set_context(context=u'zip', val=zip_code)
1818                 self.PRW_town.set_context(context=u'zip', val=zip_code)
1819                 self.PRW_state.set_context(context=u'zip', val=zip_code)
1820                 self.PRW_country.set_context(context=u'zip', val=zip_code)
1821                 return True
1822 #============================================================
1823 class cNewPatientWizard(wx.wizard.Wizard):
1824         """
1825         Wizard to create a new patient.
1826
1827         TODO:
1828         - write pages for different "themes" of patient creation
1829         - make it configurable which pages are loaded
1830         - make available sets of pages that apply to a country
1831         - make loading of some pages depend upon values in earlier pages, eg
1832           when the patient is female and older than 13 include a page about
1833           "female" data (number of kids etc)
1834
1835         FIXME: use: wizard.FindWindowById(wx.ID_FORWARD).Disable()
1836         """
1837         #--------------------------------------------------------
1838         def __init__(self, parent, title = _('Register new person'), subtitle = _('Basic demographic details') ):
1839                 """
1840                 Creates a new instance of NewPatientWizard
1841                 @param parent - The parent widget
1842                 @type parent - A wx.Window instance
1843                 """
1844                 id_wiz = wx.NewId()
1845                 wx.wizard.Wizard.__init__(self, parent, id_wiz, title) #images.getWizTest1Bitmap()
1846                 self.SetExtraStyle(wx.WS_EX_VALIDATE_RECURSIVELY)
1847                 self.__subtitle = subtitle
1848                 self.__do_layout()
1849         #--------------------------------------------------------
1850         def RunWizard(self, activate=False):
1851                 """Create new patient.
1852
1853                 activate, too, if told to do so (and patient successfully created)
1854                 """
1855                 if not wx.wizard.Wizard.RunWizard(self, self.basic_pat_details):
1856                         return False
1857
1858                 # retrieve DTD and create patient
1859                 ident = create_identity_from_dtd(dtd = self.basic_pat_details.form_DTD)
1860                 update_identity_from_dtd(identity = ident, dtd = self.basic_pat_details.form_DTD)
1861                 link_contacts_from_dtd(identity = ident, dtd = self.basic_pat_details.form_DTD)
1862                 link_occupation_from_dtd(identity = ident, dtd = self.basic_pat_details.form_DTD)
1863
1864                 if activate:
1865                         gmPerson.set_active_patient(patient = ident)
1866
1867                 return ident
1868         #--------------------------------------------------------
1869         # internal helpers
1870         #--------------------------------------------------------
1871         def __do_layout(self):
1872                 """Arrange widgets.
1873                 """
1874                 # Create the wizard pages
1875                 self.basic_pat_details = cBasicPatDetailsPage(self, self.__subtitle )
1876                 self.FitToPage(self.basic_pat_details)
1877 #============================================================
1878 class cBasicPatDetailsPageValidator(wx.PyValidator):
1879         """
1880         This validator is used to ensure that the user has entered all
1881         the required conditional values in the page (eg., to properly
1882         create an address, all the related fields must be filled).
1883         """
1884         #--------------------------------------------------------
1885         def __init__(self, dtd):
1886                 """
1887                 Validator initialization.
1888                 @param dtd The object containing the data model.
1889                 @type dtd A cFormDTD instance
1890                 """
1891                 # initialize parent class
1892                 wx.PyValidator.__init__(self)
1893                 
1894                 # validator's storage object
1895                 self.form_DTD = dtd
1896         #--------------------------------------------------------
1897         def Clone(self):
1898                 """
1899                 Standard cloner.
1900                 Note that every validator must implement the Clone() method.
1901                 """
1902                 return cBasicPatDetailsPageValidator(dtd = self.form_DTD)               # FIXME: probably need new instance of DTD ?
1903         #--------------------------------------------------------
1904         def Validate(self, parent = None):
1905                 """
1906                 Validate the contents of the given text control.
1907                 """
1908                 _pnl_form = self.GetWindow().GetParent()
1909
1910                 error = False
1911
1912                 # name fields
1913                 if _pnl_form.PRW_lastname.GetValue().strip() == '':
1914                         error = True
1915                         gmDispatcher.send(signal = 'statustext', msg = _('Must enter lastname.'))
1916                         _pnl_form.PRW_lastname.SetBackgroundColour('pink')
1917                         _pnl_form.PRW_lastname.Refresh()
1918                 else:
1919                         _pnl_form.PRW_lastname.SetBackgroundColour(wx.SystemSettings_GetColour(wx.SYS_COLOUR_WINDOW))
1920                         _pnl_form.PRW_lastname.Refresh()
1921
1922                 if _pnl_form.PRW_firstname.GetValue().strip() == '':
1923                         error = True
1924                         gmDispatcher.send(signal = 'statustext', msg = _('Must enter first name.'))
1925                         _pnl_form.PRW_firstname.SetBackgroundColour('pink')
1926                         _pnl_form.PRW_firstname.Refresh()
1927                 else:
1928                         _pnl_form.PRW_firstname.SetBackgroundColour(wx.SystemSettings_GetColour(wx.SYS_COLOUR_WINDOW))
1929                         _pnl_form.PRW_firstname.Refresh()
1930
1931                 # gender
1932                 if _pnl_form.PRW_gender.GetData() is None:
1933                         error = True
1934                         gmDispatcher.send(signal = 'statustext', msg = _('Must select gender.'))
1935                         _pnl_form.PRW_gender.SetBackgroundColour('pink')
1936                         _pnl_form.PRW_gender.Refresh()
1937                 else:
1938                         _pnl_form.PRW_gender.SetBackgroundColour(wx.SystemSettings_GetColour(wx.SYS_COLOUR_WINDOW))
1939                         _pnl_form.PRW_gender.Refresh()
1940
1941                 # dob validation
1942                 if not _pnl_form.PRW_dob.is_valid_timestamp():
1943                         error = True
1944                         msg = _('Cannot parse <%s> into proper timestamp.') % _pnl_form.PRW_dob.GetValue()
1945                         gmDispatcher.send(signal = 'statustext', msg = msg)
1946                         _pnl_form.PRW_dob.SetBackgroundColour('pink')
1947                         _pnl_form.PRW_dob.Refresh()
1948                 else:
1949                         _pnl_form.PRW_dob.SetBackgroundColour(wx.SystemSettings_GetColour(wx.SYS_COLOUR_WINDOW))
1950                         _pnl_form.PRW_dob.Refresh()
1951                                                 
1952                 # address               
1953                 is_any_field_filled = False
1954                 address_fields = (
1955                         _pnl_form.TTC_address_number,
1956                         _pnl_form.PRW_zip_code,
1957                         _pnl_form.PRW_street,
1958                         _pnl_form.PRW_town
1959                 )
1960                 for field in address_fields:
1961                         if field.GetValue().strip() != '':
1962                                 is_any_field_filled = True
1963                                 field.SetBackgroundColour(wx.SystemSettings_GetColour(wx.SYS_COLOUR_WINDOW))
1964                                 field.Refresh()
1965                                 continue
1966                         if is_any_field_filled:
1967                                 error = True
1968                                 msg = _('To properly create an address, all the related fields must be filled in.')
1969                                 gmGuiHelpers.gm_show_error(msg, _('Required fields'), gmLog.lErr)
1970                                 field.SetBackgroundColour('pink')
1971                                 field.SetFocus()
1972                                 field.Refresh()
1973
1974                 address_fields = (
1975                         _pnl_form.PRW_state,
1976                         _pnl_form.PRW_country
1977                 )
1978                 for field in address_fields:
1979                         if field.GetData() is not None:
1980                                 is_any_field_filled = True
1981                                 field.SetBackgroundColour(wx.SystemSettings_GetColour(wx.SYS_COLOUR_WINDOW))
1982                                 field.Refresh()
1983                                 continue
1984                         if is_any_field_filled:
1985                                 error = True
1986                                 msg = _('To properly create an address, all the related fields must be filled in.')
1987                                 gmGuiHelpers.gm_show_error(msg, _('Required fields'), gmLog.lErr)
1988                                 field.SetBackgroundColour('pink')
1989                                 field.SetFocus()
1990                                 field.Refresh()
1991
1992                 return (not error)
1993         #--------------------------------------------------------
1994         def TransferToWindow(self):
1995                 """
1996                 Transfer data from validator to window.
1997                 The default implementation returns False, indicating that an error
1998                 occurred.  We simply return True, as we don't do any data transfer.
1999                 """
2000                 _pnl_form = self.GetWindow().GetParent()
2001                 # fill in controls with values from self.form_DTD
2002                 _pnl_form.PRW_gender.SetData(self.form_DTD['gender'])
2003                 _pnl_form.PRW_dob.SetText(self.form_DTD['dob'])
2004                 _pnl_form.PRW_lastname.SetText(self.form_DTD['lastnames'])
2005                 _pnl_form.PRW_firstname.SetText(self.form_DTD['firstnames'])
2006                 _pnl_form.PRW_title.SetText(self.form_DTD['title'])
2007                 _pnl_form.PRW_nick.SetText(self.form_DTD['nick'])
2008                 _pnl_form.PRW_occupation.SetText(self.form_DTD['occupation'])
2009                 _pnl_form.TTC_address_number.SetValue(self.form_DTD['address_number'])
2010                 _pnl_form.PRW_street.SetText(self.form_DTD['street'])
2011                 _pnl_form.PRW_zip_code.SetText(self.form_DTD['zip_code'])
2012                 _pnl_form.PRW_town.SetText(self.form_DTD['town'])
2013                 _pnl_form.PRW_state.SetData(self.form_DTD['state'])
2014                 _pnl_form.PRW_country.SetData(self.form_DTD['country'])
2015                 _pnl_form.TTC_phone.SetValue(self.form_DTD['phone'])
2016                 return True # Prevent wxDialog from complaining.        
2017         #--------------------------------------------------------
2018         def TransferFromWindow(self):
2019                 """
2020                 Transfer data from window to validator.
2021                 The default implementation returns False, indicating that an error
2022                 occurred.  We simply return True, as we don't do any data transfer.
2023                 """
2024                 # FIXME: should be called automatically
2025                 if not self.GetWindow().GetParent().Validate():
2026                         return False
2027                 try:
2028                         _pnl_form = self.GetWindow().GetParent()
2029                         # fill in self.form_DTD with values from controls
2030                         self.form_DTD['gender'] = _pnl_form.PRW_gender.GetData()
2031                         self.form_DTD['dob'] = _pnl_form.PRW_dob.GetData()
2032                         self.form_DTD['lastnames'] = _pnl_form.PRW_lastname.GetValue()
2033                         self.form_DTD['firstnames'] = _pnl_form.PRW_firstname.GetValue()
2034                         self.form_DTD['title'] = _pnl_form.PRW_title.GetValue()
2035                         self.form_DTD['nick'] = _pnl_form.PRW_nick.GetValue()
2036                         self.form_DTD['occupation'] = _pnl_form.PRW_occupation.GetValue()
2037                         self.form_DTD['address_number'] = _pnl_form.TTC_address_number.GetValue()
2038                         self.form_DTD['street'] = _pnl_form.PRW_street.GetValue()
2039                         self.form_DTD['zip_code'] = _pnl_form.PRW_zip_code.GetValue()
2040                         self.form_DTD['town'] = _pnl_form.PRW_town.GetValue()
2041                         self.form_DTD['state'] = _pnl_form.PRW_state.GetData()
2042                         self.form_DTD['country'] = _pnl_form.PRW_country.GetData()
2043                         self.form_DTD['phone'] = _pnl_form.TTC_phone.GetValue()
2044                 except:
2045                         return False
2046                 return True
2047 #============================================================
2048 class cFormDTD:
2049         """
2050         Simple Data Transfer Dictionary class to make easy the trasfer of
2051         data between the form (view) and the business logic.
2052
2053         Maybe later consider turning this into a standard dict by
2054         {}.fromkeys([key, key, ...], default) when it becomes clear that
2055         we really don't need the added potential of a full-fledged class.
2056         """
2057         def __init__(self, fields):             
2058                 """
2059                 Initialize the DTD with the supplied field names.
2060                 @param fields The names of the fields.
2061                 @type fields A TupleType instance.
2062                 """
2063                 self.data = {}          
2064                 for a_field in fields:
2065                         self.data[a_field] = ''
2066                 
2067         def __getitem__(self, attribute):
2068                 """
2069                 Retrieve the value of the given attribute (key)
2070                 @param attribute The attribute (key) to retrieve its value for.
2071                 @type attribute a StringType instance.
2072                 """
2073                 if not self.data[attribute]:
2074                         return ''
2075                 return self.data[attribute]
2076
2077         def __setitem__(self, attribute, value):
2078                 """
2079                 Set the value of a given attribute (key).
2080                 @param attribute The attribute (key) to set its value for.
2081                 @type attribute a StringType instance.          
2082                 @param avaluee The value to set.
2083                 @rtpe attribute a StringType instance.
2084                 """
2085                 self.data[attribute] = value
2086         
2087         def __str__(self):
2088                 """
2089                 Print string representation of the DTD object.
2090                 """
2091                 return str(self.data)
2092 #============================================================
2093 # patient demographics editing classes
2094 #============================================================
2095 class cPersonDemographicsEditorNb(wx.Notebook):
2096         """Notebook displaying demographics editing pages:
2097
2098                 - Identity
2099                 - Contacts (addresses, phone numbers, etc)
2100
2101         Does NOT act on/listen to the current patient.
2102         """
2103         #--------------------------------------------------------
2104         def __init__(self, parent, id):
2105
2106                 wx.Notebook.__init__ (
2107                         self,
2108                         parent = parent,
2109                         id = id,
2110                         style = wx.NB_TOP | wx.NB_MULTILINE | wx.NO_BORDER,
2111                         name = self.__class__.__name__
2112                 )
2113
2114                 self.__identity = None
2115                 self.__do_layout()
2116                 self.SetSelection(0)
2117         #--------------------------------------------------------
2118         # public API
2119         #--------------------------------------------------------
2120         def refresh(self):
2121                 """Populate fields in pages with data from model."""
2122                 for page_idx in range(self.GetPageCount()):
2123                         page = self.GetPage(page_idx)
2124                         page.identity = self.__identity
2125
2126                 return True
2127         #--------------------------------------------------------
2128         # internal API
2129         #--------------------------------------------------------
2130         def __do_layout(self):
2131                 """Build patient edition notebook pages."""
2132
2133                 # identity page
2134                 new_page = cPersonIdentityManagerPnl(self, -1)
2135                 new_page.identity = self.__identity
2136                 self.AddPage (
2137                         page = new_page,
2138                         text = _('Identity'),
2139                         select = True
2140                 )
2141
2142                 # contacts page
2143                 new_page = cPersonContactsManagerPnl(self, -1)
2144                 new_page.identity = self.__identity
2145                 self.AddPage (
2146                         page = new_page,
2147                         text = _('Contacts'),
2148                         select = False
2149                 )
2150         #--------------------------------------------------------
2151         # properties
2152         #--------------------------------------------------------
2153         def _get_identity(self):
2154                 return self.__identity
2155
2156         def _set_identity(self, identity):
2157                 self.__identity = identity
2158
2159         identity = property(_get_identity, _set_identity)
2160 #============================================================
2161 # FIXME: support multiple occupations
2162 # FIXME: redo with wxGlade
2163
2164 class cPatOccupationsPanel(wx.Panel):
2165         """Page containing patient occupations edition fields.
2166         """
2167         def __init__(self, parent, id, ident=None):
2168                 """
2169                 Creates a new instance of BasicPatDetailsPage
2170                 @param parent - The parent widget
2171                 @type parent - A wx.Window instance
2172                 @param id - The widget id
2173                 @type id - An integer
2174                 """
2175                 wx.Panel.__init__(self, parent, id)
2176                 self.__ident = ident
2177                 self.__do_layout()
2178         #--------------------------------------------------------
2179         def __do_layout(self):
2180                 PNL_form = wx.Panel(self, -1)
2181                 # occupation
2182                 STT_occupation = wx.StaticText(PNL_form, -1, _('Occupation'))
2183                 self.PRW_occupation = cOccupationPhraseWheel(parent = PNL_form, id = -1)
2184                 self.PRW_occupation.SetToolTipString(_("primary occupation of the patient"))
2185                 # known since
2186                 STT_occupation_updated = wx.StaticText(PNL_form, -1, _('Last updated'))
2187                 self.TTC_occupation_updated = wx.TextCtrl(PNL_form, -1, style = wx.TE_READONLY)
2188
2189                 # layout input widgets
2190                 SZR_input = wx.FlexGridSizer(cols = 2, rows = 5, vgap = 4, hgap = 4)
2191                 SZR_input.AddGrowableCol(1)                             
2192                 SZR_input.Add(STT_occupation, 0, wx.SHAPED)
2193                 SZR_input.Add(self.PRW_occupation, 1, wx.EXPAND)
2194                 SZR_input.Add(STT_occupation_updated, 0, wx.SHAPED)
2195                 SZR_input.Add(self.TTC_occupation_updated, 1, wx.EXPAND)
2196                 PNL_form.SetSizerAndFit(SZR_input)
2197                 
2198                 # layout page
2199                 SZR_main = wx.BoxSizer(wx.VERTICAL)
2200                 SZR_main.Add(PNL_form, 1, wx.EXPAND)
2201                 self.SetSizer(SZR_main)
2202         #--------------------------------------------------------
2203         def set_identity(self, identity):
2204                 return self.refresh(identity=identity)
2205         #--------------------------------------------------------
2206         def refresh(self, identity=None):
2207                 if identity is not None:
2208                         self.__ident = identity
2209                 jobs = self.__ident.get_occupations()
2210                 if len(jobs) > 0:
2211                         self.PRW_occupation.SetText(jobs[0]['l10n_occupation'])
2212                         self.TTC_occupation_updated.SetValue(jobs[0]['modified_when'].strftime('%m/%Y'))
2213                 return True
2214         #--------------------------------------------------------
2215         def save(self):
2216                 if self.PRW_occupation.IsModified():
2217                         new_job = self.PRW_occupation.GetValue().strip()
2218                         jobs = self.__ident.get_occupations()
2219                         for job in jobs:
2220                                 if job['l10n_occupation'] == new_job:
2221                                         continue
2222                                 self.__ident.unlink_occupation(occupation = job['l10n_occupation'])
2223                         self.__ident.link_occupation(occupation = new_job)
2224                 return True
2225 #============================================================
2226 class cNotebookedPatEditionPanel(wx.Panel, gmRegetMixin.cRegetOnPaintMixin):
2227         """Patient demographics plugin for main notebook.
2228
2229         Hosts another notebook with pages for Identity, Contacts, etc.
2230
2231         Acts on/listens to the currently active patient.
2232         """
2233         #--------------------------------------------------------
2234         def __init__(self, parent, id):
2235                 wx.Panel.__init__ (self, parent = parent, id = id, style = wx.NO_BORDER)
2236                 gmRegetMixin.cRegetOnPaintMixin.__init__(self)
2237                 self.__do_layout()
2238                 self.__register_interests()
2239         #--------------------------------------------------------
2240         # public API
2241         #--------------------------------------------------------
2242         #--------------------------------------------------------
2243         # internal helpers
2244         #--------------------------------------------------------
2245         def __do_layout(self):
2246                 """Arrange widgets."""
2247                 self.__patient_notebook = cPersonDemographicsEditorNb(self, -1)
2248
2249                 szr_main = wx.BoxSizer(wx.VERTICAL)
2250                 szr_main.Add(self.__patient_notebook, 1, wx.EXPAND)
2251                 self.SetSizerAndFit(szr_main)
2252         #--------------------------------------------------------
2253         # event handling
2254         #--------------------------------------------------------
2255         def __register_interests(self):
2256                 gmDispatcher.connect(signal = u'pre_patient_selection', receiver = self._on_pre_patient_selection)
2257                 gmDispatcher.connect(signal = u'post_patient_selection', receiver = self._on_post_patient_selection)
2258         #--------------------------------------------------------
2259         def _on_pre_patient_selection(self):
2260                 self._schedule_data_reget()
2261         #--------------------------------------------------------
2262         def _on_post_patient_selection(self):
2263                 self._schedule_data_reget()
2264         #--------------------------------------------------------
2265         # reget mixin API
2266         #--------------------------------------------------------
2267         def _populate_with_data(self):
2268                 """Populate fields in pages with data from model."""
2269                 pat = gmPerson.gmCurrentPatient()
2270                 if pat.is_connected():
2271                         self.__patient_notebook.identity = pat
2272                 else:
2273                         self.__patient_notebook.identity = None
2274                 self.__patient_notebook.refresh()
2275                 return True
2276 #============================================================                           
2277 def create_identity_from_dtd(dtd=None):
2278         """
2279         Register a new patient, given the data supplied in the 
2280         Data Transfer Dictionary object.
2281
2282         @param basic_details_DTD Data Transfer Dictionary encapsulating all the
2283         supplied data.
2284         @type basic_details_DTD A cFormDTD instance.
2285         """
2286         new_identity = gmPerson.create_identity (
2287                 gender = dtd['gender'],
2288                 dob = dtd['dob'].get_pydt(),
2289                 lastnames = dtd['lastnames'],
2290                 firstnames = dtd['firstnames']
2291         )
2292         if new_identity is None:
2293                 _log.Log(gmLog.lErr, 'cannot create identity from %s' % str(dtd))
2294                 return None
2295         _log.Log(gmLog.lData, 'identity created: %s' % new_identity)
2296         
2297         return new_identity
2298 #============================================================
2299 def update_identity_from_dtd(identity, dtd=None):
2300         """
2301         Update patient details with data supplied by
2302         Data Transfer Dictionary object.
2303
2304         @param basic_details_DTD Data Transfer Dictionary encapsulating all the
2305         supplied data.
2306         @type basic_details_DTD A&nbs