Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Django 2 - How to register a user using email confirmation and CBVs?

Tags:

This question specifically aims for a Django 2.0 answer as the registration module isn't available (yet) for it.

More, this might seem to broad, but I often found myself in situations where I can't use any 3rd party module because ... oh well..policies. I'm sure many did. And I know that looking and putting together information taken from here or django docs was a headache.


Workflow:

Let's suppose we need the following flow:

  1. The user goes to the sign-up page and fills in the following fields: first_name, last_name and email (the email will be used as the username).
  2. The user submits the form and receives a confirmation email with an URL containing a unique token.
  3. When the user clicks on the received link, he's redirected to a page where he'll set his password. When done, he's logged in to the dashboard page.

Extra-info: The user will later log in by using his email (which is actually his username) and password.


Specific question:

  • How will the models/views (using CBVs)/forms/urls look like?
like image 476
Cajuu' Avatar asked May 11 '18 18:05

Cajuu'


People also ask

How do I create a Django signup page?

Create Sign Up Form If you are new to Django form, refer Django form tutorial. Create forms.py file if it is not present and add the following class for the sign-up form. For creating sign up form, we can use the Django UserCreationForm . Create a view to process the signup data and save that data into the database.


1 Answers

The User Model

First, you will need to create a custom User model and a custom UserManager to remove the username field and use email instead.

In models.py the UserManager should look like this:

from django.contrib.auth.models import BaseUserManager


class MyUserManager(BaseUserManager):
    """
    A custom user manager to deal with emails as unique identifiers for auth
    instead of usernames. The default that's used is "UserManager"
    """
    def _create_user(self, email, password, **extra_fields):
        """
        Creates and saves a User with the given email and password.
        """
        if not email:
            raise ValueError('The Email must be set')
        email = self.normalize_email(email)
        user = self.model(email=email, **extra_fields)
        user.set_password(password)
        user.save()
        return user

    def create_superuser(self, email, password, **extra_fields):
        extra_fields.setdefault('is_staff', True)
        extra_fields.setdefault('is_superuser', True)
        extra_fields.setdefault('is_active', True)

        if extra_fields.get('is_staff') is not True:
            raise ValueError('Superuser must have is_staff=True.')
        if extra_fields.get('is_superuser') is not True:
            raise ValueError('Superuser must have is_superuser=True.')
        return self._create_user(email, password, **extra_fields)

And the User model:

from django.db import models
from django.contrib.auth.models import AbstractBaseUser
from django.contrib.auth.models import PermissionsMixin
from django.utils.translation import ugettext_lazy as _


class User(AbstractBaseUser, PermissionsMixin):
    email = models.EmailField(unique=True, null=True)
    is_staff = models.BooleanField(
        _('staff status'),
        default=False,
        help_text=_('Designates whether the user can log into this site.'),
    )
    is_active = models.BooleanField(
        _('active'),
        default=True,
        help_text=_(
            'Designates whether this user should be treated as active. '
            'Unselect this instead of deleting accounts.'
        ),
    )
    USERNAME_FIELD = 'email'
    objects = MyUserManager()

    def __str__(self):
        return self.email

    def get_full_name(self):
        return self.email

    def get_short_name(self):
        return self.email

And finally in settings.py:

AUTH_USER_MODEL = 'your_app_name.User'

The Token Generator

Second part is to create a token generator for the email confirmation url. We can inherit the built-in PasswordResetTokenGenerator to make it easier.

Create tokens.py:

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()

The Signup Form

Then you should create a registration form to use in our views. Best way is to inherit the built-in Django's UserCreationForm and to remove the username and password fields from it and then add an email field. forms.py:

from django import forms
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth.models import User

class SignupForm(UserCreationForm):
    email = forms.EmailField(max_length=200, help_text='Required')

    class Meta:
        model = User
        fields = ('email', 'first_name', 'last_name')

The Signup View

In the sign up you should make the user inactive user.is_active = False with no password set_unusable_password() until the user complete the activation. Also, we are going to construct an activation URL and email it to the user after completing the registration.

in views.py:

from django.views import View
from django.http import HttpResponse
from django.shortcuts import render
from .forms import SignupForm
from django.contrib.sites.shortcuts import get_current_site
from django.utils.encoding import force_bytes
from django.utils.http import urlsafe_base64_encode
from .tokens import account_activation_token
from django.core.mail import EmailMessage


class Signup(View):
    def get(self, request):
        form = SignupForm()
        return render(request, 'signup.html', {'form': form})

    def post(self, request):
        form = SignupForm(request.POST)
        if form.is_valid():
            # Create an inactive user with no password:
            user = form.save(commit=False)
            user.is_active = False
            user.set_unusable_password()
            user.save()

            # Send an email to the user with the token:
            mail_subject = 'Activate your account.'
            current_site = get_current_site(request)
            uid = urlsafe_base64_encode(force_bytes(user.pk))
            token = account_activation_token.make_token(user)
            activation_link = "{0}/?uid={1}&token{2}".format(current_site, uid, token)
            message = "Hello {0},\n {1}".format(user.username, activation_link)
            to_email = form.cleaned_data.get('email')
            email = EmailMessage(mail_subject, message, to=[to_email])
            email.send()
            return HttpResponse('Please confirm your email address to complete the registration')

And of course, don't forget to create a template for you signup view.


The Activation View

Then you should create a view for the user to activate his account using the URL we created in the sign up view. We will also use the built-in Django's SetPasswordForm to allow users to set their passwords.

In views.py:

from django.contrib.auth import get_user_model, login, update_session_auth_hash
from django.contrib.auth.forms import PasswordChangeForm
from django.utils.encoding import force_bytes, force_text
from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode
from .tokens import account_activation_token

User = get_user_model()

class Activate(View):
    def get(self, request, uidb64, token):
        try:
            uid = force_text(urlsafe_base64_decode(uidb64))
            user = User.objects.get(pk=uid)
        except(TypeError, ValueError, OverflowError, User.DoesNotExist):
            user = None
        if user is not None and account_activation_token.check_token(user, token):
            # activate user and login:
            user.is_active = True
            user.save()
            login(request, user)
            
            form = PasswordChangeForm(request.user)
            return render(request, 'activation.html', {'form': form})
            
        else:
            return HttpResponse('Activation link is invalid!')
            
    def post(self, request):
        form = PasswordChangeForm(request.user, request.POST)
        if form.is_valid():
            user = form.save()
            update_session_auth_hash(request, user) # Important, to update the session with the new password
            return HttpResponse('Password changed successfully')

Again, don't forget to create a template for your activation view.


The URLs

Finally, in urls.py:

from . import views
from django.urls import path

urlpatterns = [
    ...
    path('signup/', views.signup.as_view(), name='signup'),
    path('activate/<str:uid>/<str:token>', views.activate.as_view(), name='activate'),
]

P.S. Honestly, I didn't get a chance to test all this parts together yet but don't hesitate to ask if any problem happened.

like image 50
Peter Sobhi Avatar answered Sep 17 '22 17:09

Peter Sobhi