Project

General

Profile

Download (23.7 KB) Statistics
| Branch: | Tag: | Revision:

root / extra / modules / ezldap_ui.ptl @ 22eeae99

1
'''
2
Module to provide the kind of interaction needed for a eZ publish website
3
acting as reverse proxy and passing in an HTTP header a pointer to an LDAP
4
entry.
5
'''
6

    
7
try:
8
    import ldap
9
    import ldap.modlist
10
except ImportError:
11
    ldap = None
12

    
13
import base64
14
import datetime
15
import random
16
import string
17
import urllib
18
from urlparse import urlparse
19
try:
20
    from hashlib import sha1 as sha
21
except ImportError:
22
    from sha import sha
23

    
24
from quixote import get_publisher, get_request, redirect, get_session, get_session_manager
25
from quixote.directory import Directory
26

    
27
from qommon import get_cfg, template, emails
28
from qommon import misc
29
from qommon.admin.emails import EmailsDirectory
30
from qommon.admin.texts import TextsDirectory
31
from qommon.form import *
32
from qommon.ident.password import check_password, make_password
33
from qommon import errors
34

    
35
from wcs.formdef import FormDef
36
from wcs.users import User
37

    
38
from myspace import MyspaceDirectory
39
from payments import is_payment_supported
40

    
41
import ezldap
42

    
43
def make_token():
44
    '''Create a token, checking it does not already exist in the ldap base.'''
45
    r = random.SystemRandom()
46
    ldap_basedn = get_cfg('misc', {}).get('aq-ezldap-basedn')
47
    ldap_conn = ezldap.get_ldap_conn()
48
    while True:
49
        token = ''.join([r.choice(string.letters + string.digits) for x in range(16)])
50
        ldap_result_id = ldap_conn.search(ldap_basedn, ldap.SCOPE_SUBTREE,
51
                filterstr='token=%s' % token)
52
        result_type, result_data = ldap_conn.result(ldap_result_id, all=0)
53
        if result_data == []:
54
            break
55
    return token
56

    
57
def add_token(token, dn, action, exp_days=3):
58
    ''' Add a token in the LDAP  '''
59
    expdate = datetime.datetime.utcnow() + datetime.timedelta(exp_days)
60
    ldap_conn = ezldap.get_ldap_conn()
61
    # add token into the LDAP
62
    mod_list = [(ldap.MOD_REPLACE, 'token', token),
63
            (ldap.MOD_REPLACE, 'tokenAction', action),
64
            (ldap.MOD_REPLACE, 'tokenExpiration', expdate.strftime('%Y%m%d%H%M%SZ'))]
65
    ldap_conn.modify_s(dn, mod_list)
66
    return token
67

    
68
def email_to_admins(dn, template):
69
    admins = [x for x in get_publisher().user_class.select() if x.is_admin]
70
    if not admins:
71
        return
72
    admin_emails = [x.email for x in admins if x.email]
73

    
74
    user = ezldap.EzLdapUser(dn)
75
    data = {
76
        'hostname': get_request().get_server(),
77
        'username': user.email,
78
        'email_as_username': 'True',
79
        'name': user.display_name,
80
        'email': user.email,
81
    }
82
    get_publisher().substitutions.feed(user)
83

    
84
    emails.custom_ezt_email(template, data,
85
            admin_emails, fire_and_forget = True)
86

    
87

    
88
class EzMyspaceDirectory(MyspaceDirectory):
89

    
90
    def __init__(self):
91
        self._q_exports.extend(['change_email'])
92

    
93
    def _index_buttons [html] (self, form_data):
94
        super(EzMyspaceDirectory, self)._index_buttons(form_data)
95
        '<p class="command"><a href="change_email">%s</a></p>' % _('Change my email')
96
        '<br />'
97
        '<br />'
98
        '<p>'
99
        _('You can delete your account freely from the services portal. '
100
          'This action is irreversible; it will destruct your personal '
101
          'datas and destruct the access to your request history.')
102
        ' <strong><a href="remove">%s</a></strong>.' % _('Delete My Account')
103
        '</p>'
104

    
105
    def _my_profile [html] (self, user_formdef, user):
106
        '<h3 id="my-profile">%s</h3>' % _('My Profile')
107

    
108
        TextsDirectory.get_html_text('top-of-profile')
109

    
110
        if user.form_data:
111
            get_publisher().substitutions.feed(get_request().user)
112
            data = get_publisher().substitutions.get_context_variables()
113
            TextsDirectory.get_html_text('aq-profile-presentation', data)
114
        else:
115
            '<p>%s</p>' % _('Empty profile')
116

    
117
    def submit_password(self, new_password):
118
        userPassword = ['{sha}%s' % base64.encodestring(sha(new_password).digest()).strip()]
119
        mod_list = [(ldap.MOD_REPLACE, 'userPassword', userPassword)]
120
        ezldap.get_ldap_conn().modify_s(get_session().user, mod_list)
121

    
122
    def change_email [html] (self):
123
        form = Form(enctype = 'multipart/form-data')
124
        form.add(ezldap.EzEmailWidget, 'new_email', title = _('New email'),
125
                required=True, size=30)
126
        form.add_submit('submit', _('Submit Request'))
127
        form.add_submit('cancel', _('Cancel'))
128
        if form.get_submit() == 'cancel':
129
            return redirect('.')
130
        if form.is_submitted() and not form.has_errors():
131
            new_email = form.get_widget('new_email').parse()
132
            self.change_email_submit(new_email)
133
            template.html_top(_('Change email'))
134
            TextsDirectory.get_html_text('aq-change-email-token-sent') % { 'new_email': new_email }
135
        else:
136
            template.html_top(_('Change email'))
137
            TextsDirectory.get_html_text('aq-change-email')
138
            form.render()
139

    
140
    def change_email_submit(self, new_email):
141
        data = {}
142

    
143
        token = make_token()
144
        req = get_request()
145
        base_url = '%s://%s%s' % (req.get_scheme(), req.get_server(),
146
                urllib.quote(req.environ.get('SCRIPT_NAME')))
147
        data['change_url'] = base_url + '/token/?token=%s' % token
148
        data['cancel_url'] = base_url + '/token/cancel?token=%s' % token
149
        data['new_email'] = new_email
150
        data['current_email'] = req.user.email
151

    
152
        # add the token in the LDAP
153
        add_token(token, get_session().user, "change_email;%s" % new_email)
154
        # send mail
155
        emails.custom_ezt_email('aq-change-email-request', data,
156
                data['new_email'], fire_and_forget = True)
157

    
158
    def remove [html] (self):
159
        user = get_request().user
160
        if not user or user.anonymous:
161
            raise errors.AccessUnauthorizedError()
162
        form = Form(enctype = 'multipart/form-data')
163
        form.add_submit('submit', _('Remove my account'))
164
        form.add_submit('cancel', _('Cancel'))
165
        if form.get_submit() == 'cancel':
166
            return redirect('.')
167
        if form.is_submitted() and not form.has_errors():
168
            self.remove_email_submit()
169
            template.html_top(_('Removing Account'))
170
            TextsDirectory.get_html_text('aq-remove-token-sent') % { 'email': user.email }
171
        else:
172
            template.html_top(_('Removing Account'))
173
            TextsDirectory.get_html_text('aq-remove-account')
174
            form.render()
175

    
176
    def remove_email_submit(self):
177
        data = {}
178
        token = make_token()
179
        req = get_request()
180
        base_url = '%s://%s%s' % (req.get_scheme(), req.get_server(),
181
                urllib.quote(req.environ.get('SCRIPT_NAME')))
182
        data['remove_url'] = base_url + '/token/?token=%s' % token
183
        data['cancel_url'] = base_url + '/token/cancel?token=%s' % token
184
        data['email'] = req.user.email
185
        # add the token in the LDAP
186
        add_token(token, get_session().user, "remove")
187
        # send mail
188
        emails.custom_ezt_email('aq-remove-request', data,
189
                data['email'], fire_and_forget = True)
190

    
191
class EzTokenDirectory(Directory):
192
    _q_exports = ['', 'cancel']
193

    
194
    def _q_index (self):
195
        '''token processing'''
196
        token = get_request().form.get('token')
197
        if token:
198
            result_data = self._get_ldap_entry_token(token)
199
            if result_data:
200
                return self._actions(result_data[0][1]['tokenAction'][0], result_data)
201
        template.html_top(_('Error'))
202
        return _('The token you submitted does not exist, has expired, or has been cancelled.')
203

    
204
    def _get_ldap_entry_token(self, token):
205
        ''' return a ldap result for the given token '''
206
        ldap_basedn = get_cfg('misc', {}).get('aq-ezldap-basedn')
207
        ldap_conn = ezldap.get_ldap_conn()
208
        ldap_result_id = ldap_conn.search(ldap_basedn, ldap.SCOPE_SUBTREE,
209
                    filterstr='token=%s' % token)
210
        result_type, result_data = ldap_conn.result(ldap_result_id, all=0)
211
        if result_type == ldap.RES_SEARCH_ENTRY:
212
            return result_data
213
        else:
214
            return None
215

    
216
    def _actions(self, token_action, result_data):
217
        dn = result_data[0][0]
218
        action = token_action.split(";")[0]
219
        if action == 'create':
220
            mod_list = [(ldap.MOD_REPLACE, 'actif', 'TRUE'),
221
                    (ldap.MOD_DELETE, 'token', None),
222
                    (ldap.MOD_DELETE, 'tokenAction', None),
223
                    (ldap.MOD_DELETE, 'tokenExpiration', None)]
224
            ezldap.get_ldap_conn().modify_s(dn, mod_list)
225
            email_to_admins(dn, 'new-registration-admin-notification')
226
            template.html_top(_('Welcome'))
227
            return TextsDirectory.get_html_text('account-created')
228
        elif action == 'change_email':
229
            new_email = token_action.split(";")[1]
230
            mod_list = [(ldap.MOD_REPLACE, ezldap.LDAP_EMAIL, str(new_email)),
231
                    (ldap.MOD_REPLACE, 'actif', 'TRUE'),
232
                    (ldap.MOD_DELETE, 'token', None),
233
                    (ldap.MOD_DELETE, 'tokenAction', None),
234
                    (ldap.MOD_DELETE, 'tokenExpiration', None)]
235
            ezldap.get_ldap_conn().modify_s(dn, mod_list)
236
            template.html_top(_('Email changed'))
237
            return TextsDirectory.get_html_text('aq-new-email-confirmed') % { 'new_email': new_email }
238
        elif action == 'change_password':
239
            email = result_data[0][1][ezldap.LDAP_EMAIL][0]
240
            new_password = make_password()
241
            userPassword = ['{sha}%s' % base64.encodestring(sha(new_password).digest()).strip()]
242
            mod_list = [(ldap.MOD_REPLACE, 'userPassword', userPassword),
243
                    (ldap.MOD_REPLACE, 'actif', 'TRUE'),
244
                    (ldap.MOD_DELETE, 'token', None),
245
                    (ldap.MOD_DELETE, 'tokenAction', None),
246
                    (ldap.MOD_DELETE, 'tokenExpiration', None)]
247
            ezldap.get_ldap_conn().modify_s(dn, mod_list)
248
            data = {
249
                 'username': email,
250
                 'password': new_password,
251
                 'hostname': get_request().get_server(),
252
            }
253
            emails.custom_ezt_email('new-generated-password', data, email,
254
                    exclude_current_user = False)
255
            template.html_top(_('New password sent by email'))
256
            return TextsDirectory.get_html_text('new-password-sent-by-email')
257
        elif action == 'remove':
258
            template.html_top(_('Removing Account'))
259
            # email (just before remove)
260
            email_to_admins(dn, 'notification-of-removed-account')
261
            # delete all related forms
262
            formdefs = FormDef.select()
263
            user_forms = []
264
            for formdef in formdefs:
265
                user_forms.extend(formdef.data_class().get_with_indexed_value('user_id', dn))
266
            for formdata in user_forms:
267
                formdata.remove_self()
268
            # delete the user in ldap
269
            ezldap.get_ldap_conn().delete_s(dn)
270
            # delete session and redirect to login page
271
            get_session_manager().expire_session()
272
            root_url = get_publisher().get_root_url()
273
            return redirect('%slogin' % root_url)
274

    
275
    def cancel(self):
276
        token = get_request().form.get('token')
277
        if token:
278
            result_data = self._get_ldap_entry_token(token)
279
            if result_data:
280
                dn = result_data[0][0]
281
                mod_list = [(ldap.MOD_DELETE, 'token', None),
282
                        (ldap.MOD_DELETE, 'tokenAction', None),
283
                        (ldap.MOD_DELETE, 'tokenExpiration', None)]
284
                ezldap.get_ldap_conn().modify_s(dn, mod_list)
285
                template.html_top(_('Request cancelled'))
286
                return  _('Your request has been cancelled.')
287

    
288
        template.html_top(_('Error'))
289
        return _('The token you submitted does not exist or has expired')
290

    
291

    
292
class EzRegisterDirectory(Directory):
293
    _q_exports = ['', 'passwordreset']
294

    
295
    def _q_index(self):
296
        form = Form(enctype='multipart/form-data')
297

    
298
        template.html_top(_('Registration'))
299
        head = TextsDirectory.get_html_text('new-account')
300
        form.widgets.append(HtmlWidget(head))
301

    
302
        # build password hint : "at least 4 cars, at most 12"
303
        passwords_cfg = get_cfg('passwords', {})
304
        min_len = passwords_cfg.get('min_length', 0)
305
        max_len = passwords_cfg.get('max_length', 0)
306
        if min_len and max_len:
307
            password_hint = _('At least %(min)d characters, at most %(max)d') % \
308
                    {'min': min_len, 'max': max_len }
309
        elif min_len:
310
            password_hint = _('At least %d characters') % min_len
311
        elif max_len:
312
            password_hint = _('At most %d characters') % max_len
313
        else:
314
            password_hint = ''
315

    
316
        # get required attributes from User configuration
317
        for f in User.get_formdef().fields:
318
            if f.required:
319
                kwargs = {'required': True, 'value': form.get(f.varname)}
320
                for k in f.extra_attributes:
321
                    if hasattr(f, k):
322
                        kwargs[k] = getattr(f, k)
323
                f.perform_more_widget_changes(form, kwargs)
324
                form.add(f.widget_class, f.varname, title=f.label, hint=f.hint, **kwargs)
325
        # static required fields
326
        form.add(ezldap.EzEmailWidget, ezldap.LDAP_EMAIL, title=_('Email'),
327
                value=form.get(ezldap.LDAP_EMAIL), required=True, size=30,
328
                hint=_('Remember: this will be your username'))
329
        form.add(PasswordWidget, '__password', title=_('New Password'),
330
                value=form.get('__password'), required=True, size=15,
331
                hint=password_hint)
332
        form.add(PasswordWidget, '__password2', title=_('New Password (confirm)'),
333
                value=form.get('__password2'), required=True, size=15)
334
        form.widgets.append(CaptchaWidget('__captcha'))
335
        form.add_submit('submit', _('Register'))
336
        form.add_submit('cancel', _('Cancel'))
337

    
338
        if form.get_submit() == 'cancel':
339
            return redirect('..')
340

    
341
        if form.is_submitted() and not form.has_errors():
342
            check_password(form, '__password')
343
            password = form.get_widget('__password').parse()
344
            password2 = form.get_widget('__password2').parse()
345
            if password != password2:
346
                form.set_error('__password2', _('Passwords do not match'))
347

    
348
        if form.is_submitted() and not form.has_errors():
349
            self.register_submit(form)
350
            template.html_top(_('Welcome'))
351
            return TextsDirectory.get_html_text('email-sent-confirm-creation')
352

    
353
        return form.render()
354

    
355
    def register_submit(self, form):
356
        data = {}
357
        exp_days = 3
358

    
359
        for widget in form.widgets:
360
            value = widget.parse()
361
            if value:
362
                if widget.name == '__password':
363
                    password = value
364
                    data['userPassword'] = ['{sha}%s' % \
365
                            base64.encodestring(sha(password).digest()).strip()]
366
                elif not widget.name.startswith('__'):
367
                    if widget.name.startswith('date'):
368
                        date = datetime.datetime.strptime(value, misc.date_format())
369
                        data[widget.name] = date.strftime('%Y%m%d%H%M%SZ')
370
                    else:
371
                        data[widget.name] = [value]
372

    
373
        misc_cfg = get_cfg('misc', {})
374
        expdate = datetime.datetime.utcnow() + datetime.timedelta(exp_days)
375
        data['actif'] = 'FALSE'
376
        data['token'] = make_token()
377
        data['tokenAction'] = 'create'
378
        data['tokenExpiration'] = expdate.strftime('%Y%m%d%H%M%SZ')
379
        req = get_request()
380
        base_url = '%s://%s%s' % (req.get_scheme(), req.get_server(),
381
                urllib.quote(req.environ.get('SCRIPT_NAME')))
382
        token_url = base_url + '/token/?token=%s' % data['token']
383

    
384
        data['objectClass'] = 'vincennesCitoyen'
385
        mod_list = ldap.modlist.addModlist(data)
386
        ldap_dntemplate = misc_cfg.get('aq-ezldap-dntemplate')
387
        while True:
388
            dn = ldap_dntemplate % random.randint(100000, 5000000)
389
            try:
390
                ezldap.get_ldap_conn().add_s(dn, mod_list)
391
            except ldap.ALREADY_EXISTS:
392
                continue
393
            break
394

    
395
        data = {
396
            'email': data[ezldap.LDAP_EMAIL][0],
397
            'username': data[ezldap.LDAP_EMAIL][0],
398
            'password': password,
399
            'token_url': token_url,
400
            'website': base_url,
401
            'admin_email': '',
402
        }
403
        emails.custom_ezt_email('password-subscription-notification', data,
404
                    data.get('email'), fire_and_forget = True)
405

    
406

    
407
    def passwordreset [html] (self):
408
        form = Form(enctype='multipart/form-data')
409
        form.add(EmailWidget, 'email', title=_('Email'), required=True, size=30)
410
        form.add_submit('change', _('Submit Request'))
411

    
412
        if form.is_submitted() and not form.has_errors():
413
            email = form.get_widget('email').parse()
414
            dn = self.get_dn_by_email(email)
415
            if dn:
416
                self.passwordreset_submit(email, dn)
417
                template.html_top(_('Forgotten Password'))
418
                TextsDirectory.get_html_text(str('password-forgotten-token-sent'))
419
            else:
420
                form.set_error('email', _('There is no user with that email.'))
421
                template.html_top(_('Forgotten Password'))
422
                TextsDirectory.get_html_text(str('password-forgotten-enter-username'))
423
                form.render()
424
        else:
425
            template.html_top(_('Forgotten Password'))
426
            TextsDirectory.get_html_text(str('password-forgotten-enter-username'))
427
            form.render()
428

    
429
    def get_dn_by_email(self, email):
430
        ldap_conn = ezldap.get_ldap_conn()
431
        ldap_basedn = get_cfg('misc', {}).get('aq-ezldap-basedn')
432
        ldap_result_id = ldap_conn.search(ldap_basedn, ldap.SCOPE_SUBTREE,
433
                    filterstr='%s=%s' % (ezldap.LDAP_EMAIL, email))
434
        result_type, result_data = ldap_conn.result(ldap_result_id, all=0)
435
        if result_type == ldap.RES_SEARCH_ENTRY:
436
            return result_data[0][0] # dn
437
        else:
438
            return None
439

    
440
    def passwordreset_submit(self, email, dn):
441
        token = make_token()
442
        # add token into the LDAP
443
        add_token(token, dn, "change_password")
444
        # send mail
445
        req = get_request()
446
        base_url = '%s://%s%s' % (req.get_scheme(), req.get_server(),
447
                urllib.quote(req.environ.get('SCRIPT_NAME')))
448
        data = { 'change_url': base_url + '/token/?token=%s' % token,
449
                'cancel_url': base_url + '/token/cancel?token=%s' % token,
450
                # FIXME: set the expiration date here
451
                'time': None }
452
        emails.custom_ezt_email('change-password-request', data,
453
                email, exclude_current_user = False)
454

    
455

    
456
def restore_legacy_root(root_directory):
457
    root_directory.myspace = MyspaceDirectory()
458
    from root import AlternateRegisterDirectory
459
    root_directory.register = AlternateRegisterDirectory()
460

    
461
def try_auth(root_directory):
462
    misc_cfg = get_cfg('misc', {})
463
    ldap_url = misc_cfg.get('aq-ezldap-url')
464

    
465
    if (ldap is None) or (not ldap_url) or (not get_publisher().has_site_option('ezldap')):
466
        # no ldap: restore non monkey patched classes & dir
467
        get_publisher().user_class = User
468
        restore_legacy_root(root_directory)
469
        return
470

    
471
    # activate the "magic user" (legacy+ldap)
472
    get_publisher().user_class = ezldap.EzMagicUser
473

    
474
    # reverse proxy detection
475
    reverse_url = get_request().get_header('X-WCS-ReverseProxy-URL')
476
    if reverse_url:
477
        # gorilla patching : SCRIPT_NAME/get_server/get_scheme
478
        parsed = urlparse(reverse_url)
479
        if parsed.path[-1:] == '/':
480
            get_request().environ['SCRIPT_NAME'] = parsed.path[:-1]
481
        else:
482
            get_request().environ['SCRIPT_NAME'] = parsed.path
483
        get_request().get_server = lambda clean=True: parsed.netloc
484
        get_request().get_scheme = lambda : parsed.scheme
485
    else:
486
        restore_legacy_root(root_directory)
487
        return
488

    
489
    # the request must come from the reverse-proxy IP
490
    ezldap_ip = misc_cfg.get('aq-ezldap-ip', None)
491
    if ezldap_ip and get_request().environ.get('REMOTE_ADDR') != ezldap_ip:
492
        # not a good IP: restore non monkey patched classes & dir
493
        restore_legacy_root(root_directory)
494
        return
495

    
496
    # add ldap register & token (anonymous actions)
497
    root_directory.register = EzRegisterDirectory()
498
    root_directory.token = EzTokenDirectory()
499

    
500
    # does the reverse-proxy provide the user DN ?
501
    user_dn = get_request().get_header('X-Auquotidien-Auth-Dn')
502
    session = get_session()
503
    if user_dn:
504
        # this is a LDAP user: set session, activate special myspace
505
        session.set_user(user_dn)
506
        root_directory.myspace = EzMyspaceDirectory()
507
    else:
508
        # disconnected
509
        session.set_user(None)
510
        root_directory.myspace = MyspaceDirectory()
511

    
512

    
513
EmailsDirectory.register('aq-change-email-request',
514
        N_('Request for email change'),
515
        N_('Available variables: new_email, current_email, change_url, cancel_url'),
516
        category = N_('Identification'),
517
        default_subject = N_('Change email Request'),
518
        default_body = N_('''
519
You have requested to associate your account with the email address : [new_email]
520

    
521
Warning : this email will become your username.
522

    
523
To complete the change, visit the following link:
524
[change_url]
525

    
526
If you are not the person who made this request, or you wish to cancel
527
this request, visit the following link:
528
[cancel_url]
529

    
530
If you cancel the contact email change request, your account will remain with
531
your current email ([current_email]).
532

    
533
If you do nothing, the request will lapse after 3 days.
534
'''))
535

    
536
TextsDirectory.register('aq-change-email',
537
        N_('Text when user want to change his email'),
538
        category = N_('Identification'),
539
        default = N_('''
540
You can change your email address here.
541
'''))
542

    
543
TextsDirectory.register('aq-change-email-token-sent',
544
        N_('Text after user had requested to change his email'),
545
        category = N_('Identification'),
546
        default = N_('''
547
A confirmation email has been sent to your new address (%(new_email)s).
548
Follow the instructions in that email to validate your request.
549
'''))
550

    
551
TextsDirectory.register('aq-new-email-confirmed',
552
        N_('Text when new email confirmed by user'),
553
        category = N_('Identification'),
554
        default = N_('''
555
Your email has been changed to : %(new_email)s.
556
Remember that it's your new username !
557
'''))
558

    
559
TextsDirectory.register('aq-profile-presentation',
560
        N_('Profile presentation'),
561
        hint = N_('variables: user fields varnames'),
562
        default = N_('''<p>
563
<ul>
564
  <li>Email: [session_user_email]</li>
565
</ul>
566
</p>'''))
567

    
568
EmailsDirectory.register('aq-remove-request',
569
        N_('Delete account request'),
570
        N_('Available variables: email, remove_url, cancel_url'),
571
        category = N_('Identification'),
572
        default_subject = N_('Delete account request'),
573
        default_body = N_('''
574
You have requested to *delete* your account [email]
575

    
576
Warning: this action is irreversible; it will destruct your personal
577
datas and destruct the access to your request history.
578

    
579
To complete the request, visit the following link:
580
[remove_url]
581

    
582
If you are not the person who made this request, or you wish to cancel this
583
request, visit the following link:
584
[cancel_url]
585

    
586
If you do nothing, the request will lapse after 3 days.
587
'''))
588

    
589
TextsDirectory.register('aq-remove-account',
590
        N_('Text when user want to delete his account'),
591
        category = N_('Identification'),
592
        default = N_('Are you really sure you want to remove your account?'))
593

    
594
TextsDirectory.register('aq-remove-token-sent',
595
        N_('Text after user had requested to delete his account'),
596
        category = N_('Identification'),
597
        default = N_('''
598
A confirmation email has been sent to your email address.
599
Follow the instructions in that email to validate your request.
600
'''))
601

    
(19-19/32)