Commit d201da12 authored by Bryan  BRANCOTTE's avatar Bryan BRANCOTTE
Browse files

Ability to validate validate email when creating an account

parent b1837f8c
Pipeline #26564 failed with stage
in 36 seconds
...@@ -55,6 +55,7 @@ BASETHEME_BOOTSTRAP_USER_PREFERENCE_MODEL_LOCATION_APP = "test_app_1" ...@@ -55,6 +55,7 @@ BASETHEME_BOOTSTRAP_USER_PREFERENCE_MODEL_LOCATION_APP = "test_app_1"
BASETHEME_BOOTSTRAP_USER_PREFERENCE_MODEL_NAME = "MyUserPreferences" BASETHEME_BOOTSTRAP_USER_PREFERENCE_MODEL_NAME = "MyUserPreferences"
BASETHEME_BOOTSTRAP_USERNAME_IS_EMAIL = False BASETHEME_BOOTSTRAP_USERNAME_IS_EMAIL = False
BASETHEME_BOOTSTRAP_FIRST_LAST_NAME_REQUIRED = False BASETHEME_BOOTSTRAP_FIRST_LAST_NAME_REQUIRED = False
BASETHEME_BOOTSTRAP_VALIDATE_EMAIL_BEFORE_ACTIVATION = False
################################################################################ ################################################################################
``` ```
......
...@@ -52,6 +52,7 @@ Quick start ...@@ -52,6 +52,7 @@ Quick start
BASETHEME_BOOTSTRAP_USER_PREFERENCE_MODEL_NAME = "MyUserPreferences" BASETHEME_BOOTSTRAP_USER_PREFERENCE_MODEL_NAME = "MyUserPreferences"
BASETHEME_BOOTSTRAP_USERNAME_IS_EMAIL = False BASETHEME_BOOTSTRAP_USERNAME_IS_EMAIL = False
BASETHEME_BOOTSTRAP_FIRST_LAST_NAME_REQUIRED = False BASETHEME_BOOTSTRAP_FIRST_LAST_NAME_REQUIRED = False
BASETHEME_BOOTSTRAP_VALIDATE_EMAIL_BEFORE_ACTIVATION = False
################################################################################ ################################################################################
......
from django.conf import settings
def is_username_is_email():
try:
return settings.BASETHEME_BOOTSTRAP_USERNAME_IS_EMAIL
except AttributeError:
return False
def is_first_last_name_required():
try:
return settings.BASETHEME_BOOTSTRAP_FIRST_LAST_NAME_REQUIRED
except AttributeError:
return False
def is_validating_email():
try:
return settings.BASETHEME_BOOTSTRAP_VALIDATE_EMAIL_BEFORE_ACTIVATION
except AttributeError:
return False
from django.conf import settings
from django.contrib.auth import get_user_model, forms as auth_forms from django.contrib.auth import get_user_model, forms as auth_forms
from django.db.models import Q from django.db.models import Q
from django.forms import ModelForm from django.forms import ModelForm
...@@ -6,19 +6,7 @@ from django.urls import reverse ...@@ -6,19 +6,7 @@ from django.urls import reverse
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from basetheme_bootstrap.default_settings import is_username_is_email, is_first_last_name_required
def is_username_is_email():
try:
return settings.BASETHEME_BOOTSTRAP_USERNAME_IS_EMAIL
except AttributeError:
return False
def is_first_last_name_required():
try:
return settings.BASETHEME_BOOTSTRAP_FIRST_LAST_NAME_REQUIRED
except AttributeError:
return False
class CleanUsernameAndSuggestReset: class CleanUsernameAndSuggestReset:
......
import os import os
import re
from django.conf import settings from django.conf import settings
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.contrib.auth.models import AnonymousUser from django.contrib.auth.models import AnonymousUser, Group
from django.core import mail
from django.core.cache import cache from django.core.cache import cache
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.test import TestCase, RequestFactory, override_settings from django.test import TestCase, RequestFactory, override_settings
from django.urls import reverse from django.urls import reverse
from basetheme_bootstrap import user_preferences from basetheme_bootstrap import user_preferences, tokens
from basetheme_bootstrap.user_preferences import get_user_preferences_for_user from basetheme_bootstrap.user_preferences import get_user_preferences_for_user
...@@ -138,6 +140,99 @@ class SignUpWithFirstLastNameRequiredTests(TestCase): ...@@ -138,6 +140,99 @@ class SignUpWithFirstLastNameRequiredTests(TestCase):
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
@override_settings(
BASETHEME_BOOTSTRAP_VALIDATE_EMAIL_BEFORE_ACTIVATION=True,
PASSWORD_RESET_TIMEOUT_DAYS=1,
)
class SignUpWithValidationTests(TestCase):
def setUp(self):
cache.clear()
get_user_model().objects.create(
username="root",
)
self.data = {
'username': "userAAA",
'email': "userAAA@mp.com",
'password1': "user@mp.comuser@mp.comuser@mp.comuser@mp.com",
'password2': "user@mp.comuser@mp.comuser@mp.comuser@mp.com",
'first_name': "user"
}
def test_sign_up_form_view(self):
response = self.client.post(reverse('basetheme_bootstrap:signup'), self.data)
self.assertEqual(response.status_code, 200)
user = get_user_model().objects.last()
self.assertFalse(user.is_active)
self.assertIn(Group.objects.get(name="PendingAccountUser"), user.groups.all())
self.assertEqual(len(mail.outbox), 1)
activate_link_example = reverse('basetheme_bootstrap:activate', args=["AAA", "AAA-AAA"])
activate_link_example = activate_link_example.replace("AAA", "([a-zA-Z0-9]+)")
m = re.findall(activate_link_example, mail.outbox[0].body)
self.assertEqual(len(m), 1)
self.client.post(reverse('basetheme_bootstrap:activate', args=[m[0][0], m[0][1] + "-" + m[0][2]]))
user = get_user_model().objects.last()
self.assertTrue(user.is_active)
self.assertNotIn(Group.objects.get(name="PendingAccountUser"), user.groups.all())
## test an account cannot be re-activated with the link:
user.is_active=False
user.save()
self.client.post(reverse('basetheme_bootstrap:activate', args=[m[0][0], m[0][1] + "-" + m[0][2]]))
self.assertFalse(get_user_model().objects.last().is_active)
def test_sign_up_with_user_pending_resend_email(self):
user_count = get_user_model().objects.count()
response = self.client.post(reverse('basetheme_bootstrap:signup'), self.data)
self.assertEqual(get_user_model().objects.count(), user_count + 1)
self.assertEqual(response.status_code, 200)
self.assertEqual(len(mail.outbox), 1)
response = self.client.post(reverse('basetheme_bootstrap:signup'), self.data)
self.assertEqual(get_user_model().objects.count(), user_count + 1)
self.assertEqual(response.status_code, 200)
self.assertEqual(len(mail.outbox), 2)
activate_link_example = reverse('basetheme_bootstrap:activate', args=["AAA", "AAA-AAA"])
activate_link_example = activate_link_example.replace("AAA", "([a-zA-Z0-9]+)")
m = re.findall(activate_link_example, mail.outbox[-1].body)
self.assertEqual(len(m), 1)
def test_activate_too_late_with_user_pending_resend_email(self):
actual_account_activation_token = tokens.account_activation_token
class MockedTokenGenerator(tokens.TokenGenerator):
def _today(self):
from datetime import date, timedelta
# Used for mocking in tests
return date.today() - timedelta(days=2)
tokens.account_activation_token = MockedTokenGenerator()
user_count = get_user_model().objects.count()
response = self.client.post(reverse('basetheme_bootstrap:signup'), self.data)
self.assertEqual(get_user_model().objects.count(), user_count + 1)
self.assertEqual(response.status_code, 200)
self.assertFalse(get_user_model().objects.last().is_active)
tokens.account_activation_token = actual_account_activation_token
self.assertEqual(len(mail.outbox), 1)
activate_link_example = reverse('basetheme_bootstrap:activate', args=["AAA", "AAA-AAA"])
activate_link_example = activate_link_example.replace("AAA", "([a-zA-Z0-9]+)")
m = re.findall(activate_link_example, mail.outbox[0].body)
self.assertEqual(len(m), 1)
self.client.post(reverse('basetheme_bootstrap:activate', args=[m[0][0], m[0][1] + "-" + m[0][2]]))
self.assertFalse(get_user_model().objects.last().is_active)
self.assertEqual(len(mail.outbox), 2)
activate_link_example = reverse('basetheme_bootstrap:activate', args=["AAA", "AAA-AAA"])
activate_link_example = activate_link_example.replace("AAA", "([a-zA-Z0-9]+)")
m = re.findall(activate_link_example, mail.outbox[1].body)
self.assertEqual(len(m), 1)
class TestWithTemplatesInPlace(SignUpTests): class TestWithTemplatesInPlace(SignUpTests):
def setUp(self): def setUp(self):
......
from django.contrib.auth.tokens import PasswordResetTokenGenerator
from django.utils import six
class TokenGenerator(PasswordResetTokenGenerator):
def _make_hash_value(self, user, timestamp):
return (
six.text_type(user.pk) + six.text_type(timestamp) +
six.text_type(user.is_active)
)
account_activation_token = TokenGenerator()
...@@ -19,6 +19,8 @@ urlpatterns = [ ...@@ -19,6 +19,8 @@ urlpatterns = [
url(r'^accounts/signup/$', views.signup, name='signup'), url(r'^accounts/signup/$', views.signup, name='signup'),
url(r'^accounts/edit/$', views.user_update, name='user-update'), url(r'^accounts/edit/$', views.user_update, name='user-update'),
url(r'^accounts/remove/$', views.user_delete, name='user-delete'), url(r'^accounts/remove/$', views.user_delete, name='user-delete'),
url(r'^accounts/activate/(?P<uidb64>[0-9A-Za-z_\-]+)/(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$',
views.activate, name='activate'),
################################################################################ ################################################################################
# Lost password # Lost password
################################################################################ ################################################################################
......
...@@ -6,15 +6,21 @@ from django.contrib import messages ...@@ -6,15 +6,21 @@ from django.contrib import messages
from django.contrib.auth import update_session_auth_hash, authenticate, login, get_user_model from django.contrib.auth import update_session_auth_hash, authenticate, login, get_user_model
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.contrib.auth.forms import PasswordChangeForm from django.contrib.auth.forms import PasswordChangeForm
from django.contrib.auth.models import Group
from django.core.mail import send_mail from django.core.mail import send_mail
from django.db.models import ProtectedError from django.db.models import ProtectedError
from django.forms import widgets from django.forms import widgets
from django.http import HttpResponseForbidden from django.http import HttpResponseForbidden
from django.shortcuts import render, redirect from django.shortcuts import render, redirect
from django.template import TemplateDoesNotExist from django.template import TemplateDoesNotExist
from django.urls import reverse
from django.utils.encoding import force_bytes, force_text
from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode
from django.utils.translation import ugettext from django.utils.translation import ugettext
from basetheme_bootstrap import tokens
from basetheme_bootstrap import user_preferences from basetheme_bootstrap import user_preferences
from basetheme_bootstrap.default_settings import is_validating_email
from basetheme_bootstrap.forms import UserCreationFormWithMore, \ from basetheme_bootstrap.forms import UserCreationFormWithMore, \
MyUserChangeForm, UserDeleteForm MyUserChangeForm, UserDeleteForm
...@@ -53,35 +59,38 @@ def signup(request): ...@@ -53,35 +59,38 @@ def signup(request):
if request.method == 'POST': if request.method == 'POST':
form = UserCreationFormWithMore(request.POST) form = UserCreationFormWithMore(request.POST)
if form.is_valid(): if form.is_valid():
auto_active = not is_validating_email()
user = form.save() user = form.save()
if get_user_model().objects.filter(pk__gt=1).count() == 0: if get_user_model().objects.filter(pk__gt=1).count() == 0:
user.is_superuser = True user.is_superuser = True
user.is_staff = True user.is_staff = True
user.save() user.is_active = True
Group.objects.get_or_create(name="PendingAccountUser")
else:
if not auto_active:
g, created = Group.objects.get_or_create(name="PendingAccountUser")
user.groups.add(g)
user.is_active = auto_active
user.save()
send_account_created(request, user, auto_active=auto_active)
if not auto_active:
return account_is_pending_view(request, email=request.POST['email'])
username = user.username username = user.username
raw_password = form.cleaned_data.get('password1') raw_password = form.cleaned_data.get('password1')
user = authenticate(username=username, password=raw_password) user = authenticate(username=username, password=raw_password)
request.user = user request.user = user
try:
send_mail(
subject=ugettext('Account successfully created'),
message=ugettext(
'Dear %(first_name)s %(last_name)s\n\n'
'Your account have successfully been created on %(joined)s.\n\nBest regards') % dict(
first_name=request.user.first_name,
last_name=request.user.last_name,
joined=str(request.user.date_joined),
),
from_email=settings.DEFAULT_FROM_EMAIL,
recipient_list=[request.user.email],
fail_silently=False,
)
except Exception as e:
logging.exception("Sending email to user %i failed" % user.pk)
login(request, user) login(request, user)
return redirect('home') return redirect('home')
else:
user = get_user_model().objects.filter(groups__name="PendingAccountUser",
email=request.POST['email']).first()
if user is not None:
send_account_created(request, user, auto_active=not is_validating_email())
return account_is_pending_view(request, email=request.POST['email'])
else: else:
if not request.user.is_anonymous: if not request.user.is_anonymous:
return HttpResponseForbidden() return HttpResponseForbidden()
...@@ -89,6 +98,57 @@ def signup(request): ...@@ -89,6 +98,57 @@ def signup(request):
return render(request, 'registration/signup.html', {'form': form}) return render(request, 'registration/signup.html', {'form': form})
def account_is_pending_view(request, email):
return render(request, 'basetheme_bootstrap/simple_message_page.html', {
'page_title': ugettext('Account activation pending'),
'message': ugettext('An email was sent with a link to validate your account, '
'please click on the link to enable your account.'),
'sub_message': ugettext('The email has been addressed to %s.') % email,
})
def send_account_created(request, user, auto_active=False):
try:
activation_link = request.scheme + "://" + request.get_host()
activation_link += reverse('basetheme_bootstrap:activate', kwargs={
'uidb64': urlsafe_base64_encode(force_bytes(user.pk)).decode(),
'token': tokens.account_activation_token.make_token(user)
})
if auto_active:
message = ugettext(
'Dear %(first_name)s %(last_name)s\n\n'
'Your account have successfully been created on %(joined)s.'
'\n\n'
'Best regards') % dict(
first_name=user.first_name,
last_name=user.last_name,
joined=str(user.date_joined),
)
else:
message = ugettext(
'Dear %(first_name)s %(last_name)s\n\n'
'Your account have successfully been created on %(joined)s.'
'\n\n'
'Please click on the link to confirm your registration\n'
'%(activation_link)s'
'\n\n'
'Best regards') % dict(
first_name=user.first_name,
last_name=user.last_name,
joined=str(user.date_joined),
activation_link=activation_link,
)
send_mail(
subject=ugettext('Account successfully created'),
message=message,
from_email=settings.DEFAULT_FROM_EMAIL,
recipient_list=[user.email],
fail_silently=False,
)
except Exception as e:
logging.exception("Sending email to user %i failed" % user.pk)
def user_update(request): def user_update(request):
if request.method == 'POST': if request.method == 'POST':
form = MyUserChangeForm(instance=request.user, data=request.POST) form = MyUserChangeForm(instance=request.user, data=request.POST)
...@@ -165,3 +225,31 @@ def account_detail(request): ...@@ -165,3 +225,31 @@ def account_detail(request):
'form_prefs': form_prefs, 'form_prefs': form_prefs,
'btn_classes': 'pull-right float-right' 'btn_classes': 'pull-right float-right'
}) })
def activate(request, uidb64, token):
try:
uid = force_text(urlsafe_base64_decode(uidb64))
user = get_user_model().objects.get(pk=uid)
except(TypeError, ValueError, OverflowError, get_user_model().DoesNotExist):
user = None
if user is not None and user.groups.filter(name="PendingAccountUser").exists():
if tokens.account_activation_token.check_token(user, token):
user.is_active = True
user.groups.remove(user.groups.get(name="PendingAccountUser"))
user.save()
login(request, user)
# return redirect('home')
# return HttpResponse('Thank you for your email confirmation. Now you can login your account.')
return render(request, 'basetheme_bootstrap/simple_message_page.html', {
'page_title': ugettext('Account activated'),
'message': ugettext('Thank you for your email confirmation, you account have been activated '
'and you are now logged in.'),
})
else:
send_account_created(request, user)
return account_is_pending_view(request, email=user.email)
else:
return render(request, 'basetheme_bootstrap/simple_message_page.html', {
'page_title': ugettext('Activation link is invalid!'),
})
...@@ -7,7 +7,7 @@ readme = open('README.rst').read() ...@@ -7,7 +7,7 @@ readme = open('README.rst').read()
setup( setup(
name='django-basetheme-bootstrap', name='django-basetheme-bootstrap',
version='0.2.29', version='0.2.30',
description='Django Basetheme Bootstrap', description='Django Basetheme Bootstrap',
long_description=readme, long_description=readme,
author='Bryan Brancotte', author='Bryan Brancotte',
......
...@@ -138,6 +138,7 @@ BASETHEME_BOOTSTRAP_USER_PREFERENCE_MODEL_LOCATION_APP = "test_app_1" ...@@ -138,6 +138,7 @@ BASETHEME_BOOTSTRAP_USER_PREFERENCE_MODEL_LOCATION_APP = "test_app_1"
BASETHEME_BOOTSTRAP_USER_PREFERENCE_MODEL_NAME = "MyUserPreferences" BASETHEME_BOOTSTRAP_USER_PREFERENCE_MODEL_NAME = "MyUserPreferences"
# BASETHEME_BOOTSTRAP_USERNAME_IS_EMAIL = True # default is False # BASETHEME_BOOTSTRAP_USERNAME_IS_EMAIL = True # default is False
# BASETHEME_BOOTSTRAP_FIRST_LAST_NAME_REQUIRED = True # default is False # BASETHEME_BOOTSTRAP_FIRST_LAST_NAME_REQUIRED = True # default is False
# BASETHEME_BOOTSTRAP_VALIDATE_EMAIL_BEFORE_ACTIVATION = True # default is False
################################################################################ ################################################################################
# Various debug stuff # Various debug stuff
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment