diff --git a/userena/managers.py b/userena/managers.py index ee8c48f6..c6400b31 100644 --- a/userena/managers.py +++ b/userena/managers.py @@ -103,6 +103,30 @@ def create_userena_profile(self, user): return self.create(user=user, activation_key=activation_key) + def reissue_activation(self, activation_key): + """ + Creates a new ``activation_key`` resetting activation timeframe when + users let the previous key expire. + + :param activation_key: + String containing the secret SHA1 activation key. + + """ + try: + userena = self.get(activation_key=activation_key) + except self.model.DoesNotExist: + return False + try: + salt, new_activation_key = generate_sha1(userena.user.username) + userena.activation_key = new_activation_key + userena.save(using=self._db) + userena.user.date_joined = get_datetime_now() + userena.user.save(using=self._db) + userena.send_activation_email() + return True + except Exception,e: + return False + def activate_user(self, activation_key): """ Activate an :class:`User` by supplying a valid ``activation_key``. @@ -136,6 +160,25 @@ def activate_user(self, activation_key): return user return False + def check_expired_activation(self, activation_key): + """ + Check if ``activation_key`` is still valid. + + Raises a ``self.model.DoesNotExist`` exception if key is not present or + ``activation_key`` is not a valid string + + :param activation_key: + String containing the secret SHA1 for a valid activation. + + :return: + True if the ket has expired, False if still valid. + + """ + if SHA1_RE.search(activation_key): + userena = self.get(activation_key=activation_key) + return userena.activation_key_expired() + raise self.model.DoesNotExist + def confirm_email(self, confirmation_key): """ Confirm an email address by checking a ``confirmation_key``. diff --git a/userena/settings.py b/userena/settings.py index 8df4b80c..da6e394c 100644 --- a/userena/settings.py +++ b/userena/settings.py @@ -34,6 +34,10 @@ 'USERENA_ACTIVATION_NOTIFY_DAYS', 5) +USERENA_ACTIVATION_RETRY = getattr(settings, + 'USERENA_ACTIVATION_RETRY', + False) + USERENA_ACTIVATED = getattr(settings, 'USERENA_ACTIVATED', 'ALREADY_ACTIVATED') diff --git a/userena/templates/userena/activate_retry.html b/userena/templates/userena/activate_retry.html new file mode 100644 index 00000000..dbdf55d7 --- /dev/null +++ b/userena/templates/userena/activate_retry.html @@ -0,0 +1,12 @@ +{% extends 'userena/base_userena.html' %} +{% load i18n %} +{% load url from future %} + + +{% block title %}{% trans "Account activation failed." %}{% endblock %} +{% block content_title %}

{% trans "Your account could not be activated..." %}

{% endblock %} + +{% block content %} +

{% trans "Your account could not be activated because activation link is expired." %}

+

{% trans "Request a new activation link." %}

+{% endblock %} diff --git a/userena/templates/userena/activate_retry_success.html b/userena/templates/userena/activate_retry_success.html new file mode 100644 index 00000000..5ee3a0f2 --- /dev/null +++ b/userena/templates/userena/activate_retry_success.html @@ -0,0 +1,13 @@ +{% extends 'userena/base_userena.html' %} +{% load i18n %} +{% load url from future %} + + +{% block title %}{% trans "Account re-activation succeded." %}{% endblock %} +{% block content_title %}

{% trans "Account re-activation" %}

{% endblock %} + +{% block content %} +

{% blocktrans %}You requested a new activation of your account..{% endblocktrans %}

+

{% blocktrans %}You have been sent an e-mail with an activation link to the supplied email.{% endblocktrans %}

+

{% blocktrans %}We will store your signup information for {{ userena_activation_days }} days on our server. {% endblocktrans %}

+{% endblock %} diff --git a/userena/tests/views.py b/userena/tests/views.py index ad684c4b..155435ac 100644 --- a/userena/tests/views.py +++ b/userena/tests/views.py @@ -1,7 +1,9 @@ +from datetime import datetime, timedelta from django.core.urlresolvers import reverse from django.core import mail from django.contrib.auth.forms import PasswordChangeForm from django.conf import settings +from django.test.utils import override_settings from userena import forms from userena import settings as userena_settings @@ -33,6 +35,65 @@ def test_valid_activation(self): user = User.objects.get(email='alice@example.com') self.failUnless(user.is_active) + def test_activation_expired_retry(self): + """ A ``GET`` to the activation view when activation link is expired """ + # First, register an account. + userena_settings.USERENA_ACTIVATION_RETRY = True + self.client.post(reverse('userena_signup'), + data={'username': 'alice', + 'email': 'alice@example.com', + 'password1': 'swordfish', + 'password2': 'swordfish', + 'tos': 'on'}) + user = User.objects.get(email='alice@example.com') + user.date_joined = datetime.today() - timedelta(days=30) + user.save() + response = self.client.get(reverse('userena_activate', + kwargs={'activation_key': user.userena_signup.activation_key})) + self.assertContains(response, "Request a new activation link") + + user = User.objects.get(email='alice@example.com') + self.failUnless(not user.is_active) + userena_settings.USERENA_ACTIVATION_RETRY = False + + def test_retry_activation_ask(self): + """ Ask for a new activation link """ + # First, register an account. + userena_settings.USERENA_ACTIVATION_RETRY = True + self.client.post(reverse('userena_signup'), + data={'username': 'alice', + 'email': 'alice@example.com', + 'password1': 'swordfish', + 'password2': 'swordfish', + 'tos': 'on'}) + user = User.objects.get(email='alice@example.com') + user.date_joined = datetime.today() - timedelta(days=30) + user.save() + old_key = user.userena_signup.activation_key + response = self.client.get(reverse('userena_activate_retry', + kwargs={'activation_key': old_key})) + + # We must reload the object from database to get the new key + user = User.objects.get(email='alice@example.com') + self.assertContains(response, "Account re-activation succeded") + + self.failIfEqual(old_key, user.userena_signup.activation_key) + user = User.objects.get(email='alice@example.com') + self.failUnless(not user.is_active) + + self.failUnlessEqual(len(mail.outbox), 2) + self.assertEqual(mail.outbox[1].to, ['alice@example.com']) + self.assertTrue(mail.outbox[1].body.find("activate your account ")>-1) + + response = self.client.get(reverse('userena_activate', + kwargs={'activation_key': user.userena_signup.activation_key})) + self.assertRedirects(response, + reverse('userena_profile_detail', kwargs={'username': user.username})) + + user = User.objects.get(email='alice@example.com') + self.failUnless(user.is_active) + userena_settings.USERENA_ACTIVATION_RETRY = False + def test_invalid_activation(self): """ A ``GET`` to the activation view with a wrong ``activation_key``. diff --git a/userena/urls.py b/userena/urls.py index 13457c06..c7d7a2c6 100644 --- a/userena/urls.py +++ b/userena/urls.py @@ -49,6 +49,11 @@ userena_views.activate, name='userena_activate'), + # Retry activation + url(r'^activate/retry/(?P\w+)/$', + userena_views.activate_retry, + name='userena_activate_retry'), + # Change email and confirm it url(r'^(?P[\.\w-]+)/email/$', userena_views.email_change, diff --git a/userena/views.py b/userena/views.py index c1216e63..09342731 100644 --- a/userena/views.py +++ b/userena/views.py @@ -146,6 +146,7 @@ def signup(request, signup_form=SignupForm, @secure_required def activate(request, activation_key, template_name='userena/activate_fail.html', + retry_template_name='userena/activate_retry.html', success_url=None, extra_context=None): """ Activate a user with an activation key. @@ -155,6 +156,8 @@ def activate(request, activation_key, activated. After a successful activation the view will redirect to ``success_url``. If the SHA1 is not found, the user will be shown the ``template_name`` template displaying a fail message. + If the SHA1 is found but expired, ``retry_template_name`` is used instead, + so the user can proceed to :func:`activate_retry` to get a new actvation key. :param activation_key: String of a SHA1 string of 40 characters long. A SHA1 is always 160bit @@ -164,7 +167,12 @@ def activate(request, activation_key, :param template_name: String containing the template name that is used when the ``activation_key`` is invalid and the activation fails. Defaults to - ``userena/activation_fail.html``. + ``userena/activate_fail.html``. + + :param retry_template_name: + String containing the template name that is used when the + ``activation_key`` is expired. Defaults to + ``userena/activate_retry.html``. :param success_url: String containing the URL where the user should be redirected to after @@ -177,25 +185,81 @@ def activate(request, activation_key, context. Default to an empty dictionary. """ - user = UserenaSignup.objects.activate_user(activation_key) - if user: - # Sign the user in. - auth_user = authenticate(identification=user.email, - check_password=False) - login(request, auth_user) + try: + if (not UserenaSignup.objects.check_expired_activation(activation_key) + or not userena_settings.USERENA_ACTIVATION_RETRY): + user = UserenaSignup.objects.activate_user(activation_key) + if user: + # Sign the user in. + auth_user = authenticate(identification=user.email, + check_password=False) + login(request, auth_user) - if userena_settings.USERENA_USE_MESSAGES: - messages.success(request, _('Your account has been activated and you have been signed in.'), - fail_silently=True) + if userena_settings.USERENA_USE_MESSAGES: + messages.success(request, _('Your account has been activated and you have been signed in.'), + fail_silently=True) - if success_url: redirect_to = success_url % {'username': user.username } - else: redirect_to = reverse('userena_profile_detail', - kwargs={'username': user.username}) - return redirect(redirect_to) - else: + if success_url: redirect_to = success_url % {'username': user.username } + else: redirect_to = reverse('userena_profile_detail', + kwargs={'username': user.username}) + return redirect(redirect_to) + else: + if not extra_context: extra_context = dict() + return ExtraContextTemplateView.as_view(template_name=template_name, + extra_context=extra_context)( + request) + else: + if not extra_context: extra_context = dict() + extra_context['activation_key'] = activation_key + return ExtraContextTemplateView.as_view(template_name=retry_template_name, + extra_context=extra_context)(request) + except UserenaSignup.DoesNotExist,e: if not extra_context: extra_context = dict() return ExtraContextTemplateView.as_view(template_name=template_name, - extra_context=extra_context)(request) + extra_context=extra_context)(request) + +@secure_required +def activate_retry(request, activation_key, + template_name='userena/activate_retry_success.html', + extra_context=None): + """ + Reissue a new ``activation_key`` for the user with the expired + ``activation_key``. + + If ``activation_key`` does not exists, or ``USERENA_ACTIVATION_RETRY`` is + set to False and for any other error condition user is redirected to + :func:`activate` for error message display. + + :param activation_key: + String of a SHA1 string of 40 characters long. A SHA1 is always 160bit + long, with 4 bits per character this makes it --160/4-- 40 characters + long. + + :param template_name: + String containing the template name that is used when new + ``activation_key`` has been created. Defaults to + ``userena/activate_retry_success.html``. + + :param extra_context: + Dictionary containing variables which could be added to the template + context. Default to an empty dictionary. + + """ + if not userena_settings.USERENA_ACTIVATION_RETRY: + return redirect(reverse('userena_activate', args=(activation_key,))) + try: + if UserenaSignup.objects.check_expired_activation(activation_key): + new_key = UserenaSignup.objects.reissue_activation(activation_key) + if new_key: + if not extra_context: extra_context = dict() + return ExtraContextTemplateView.as_view(template_name=template_name, + extra_context=extra_context)(request) + else: + return redirect(reverse('userena_activate',args=(activation_key,))) + else: + return redirect(reverse('userena_activate',args=(activation_key,))) + except UserenaSignup.DoesNotExist: + return redirect(reverse('userena_activate',args=(activation_key,))) @secure_required def email_confirm(request, confirmation_key,