Django and Ajax: Robust authentication and authorization system with real-time form validations for web applications - 3

In the previous part, we went through setting up custom signing in flow with our custom authentication backend thereby fulfilling this part of the specification:

..Users can either login with any of Email/Password or Username/Password combination...

We will continue implementing the features required by the specification in this part. Specifically, we'll be handling students registration flow.

For this project β€” to adhere to the specification β€” our registration will be in these phases:

  • Users fill in their details in the registration form;

  • If valid, a mail will be sent to the provided e-mail address during registration; then

  • On verification, by clicking the provided link which is only valid for one transaction, users can login.

Source code

The source code to this point is hosted on github while the source code for the entire application is:

GitHub logo Sirneij / django_real_time_validation

Django and Ajax: Robust authentication and authorization system with real-time form validations for web applications

django_real_time_validation

Django and Ajax: Robust authentication and authorization system with real-time form validations for web applications



Step 5: Student registration form

Let's open up aacounts/forms.py in a text editor and append the following:

# accounts > forms.py
...
from django.contrib.auth import get_user_model
from django.contrib.auth.forms import UserCreationForm

...

class StudentRegistrationForm(UserCreationForm):
    username = forms.CharField(
        widget=forms.TextInput(attrs={"placeholder": "Student number e.g. CPE/34/2367"})
    )

    def __init__(self, *args, **kwargs):
        super(StudentRegistrationForm, self).__init__(*args, **kwargs)
        for visible in self.visible_fields():
            visible.field.widget.attrs["class"] = "validate"

    class Meta:
        model = get_user_model()
        fields = (
            "first_name",
            "last_name",
            "username",
            "email",
            "level",
            "gender",
            "password1",
            "password2",
        )

It's a simple form that inherits from Django's UserCreationForm. This is one of the built-in forms in the framework. UserCreationForm is simply a ModelForm with three fields inherited from user model β€” username, password1 and password2 β€” for creating users. This might not be necessary though since our form will be processed using AJAX. Still prefer using it.

Step 6: Write utils.py for student registration

To address all the concerns for student registration in the specification:

...For students, their usernames must start with CPE and must be at least 9 characters long not counting any forward slash that it contains...

we'll begin writing some helper functions to ensure they are met.

Navigate to accounts/utils.py and populate it with:

# accounts > utils.py

import re

from django.contrib.auth import get_user_model
from django.contrib.auth.password_validation import validate_password
from django.core import exceptions

USERNAME_MIN_LENGTH = 9


def is_valid_username(username):
    if get_user_model().objects.filter(username=username).exists():
        return False
    if not username.lower().startswith("cpe"):
        return False
    if len(username.replace("/", "")) < USERNAME_MIN_LENGTH:
        return False
    if not username.isalnum():
        return False
    return True


def is_valid_password(password, user):
    try:
        validate_password(password, user=user)
    except exceptions.ValidationError:
        return False
    return True


def is_valid_email(email):
    if get_user_model().objects.filter(email=email).exists():
        return False
    if not re.match(r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b", email):
        return False
    if email is None:
        return False
    return True


def validate_username(username):
    if get_user_model().objects.filter(username=username).exists():
        return {
            "success": False,
            "reason": "User with that matriculation number already exists",
        }
    if not isinstance(username, six.string_types):

        return {
            "success": False,
            "reason": "Matriculation number should be alphanumeric",
        }

    if len(username.replace("/", "")) < USERNAME_MIN_LENGTH:
        return {
            "success": False,
            "reason": "Matriculation number too long",
        }

    if not username.isalnum():

        return {
            "success": False,
            "reason": "Matriculation number should be alphanumeric",
        }

    if not username.lower().startswith("cpe"):
        return {
            "success": False,
            "reason": "Matriculation number is not valid",
        }

    return {
        "success": True,
    }


def validate_email(email):
    if get_user_model().objects.filter(email=email).exists():
        return {"success": False, "reason": "Email Address already exists"}
    if not re.match(r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b", email):
        return {"success": False, "reason": "Invalid Email Address"}
    if email is None:
        return {"success": False, "reason": "Email is required."}
    return {"success": True}

Waoh πŸ’”!!! That is to much to decipher. Sorry buddy, let's walk through it. In the is_valid_username function, we are only ensuring that all tests are passed by the user:

  • No duplicate username:
...
  if get_user_model().objects.filter(username=username).exists():
        return False
  • Username for students must start with CPE:
if not username.lower().startswith("cpe"):
        return False
  • Users' username must not be less than 9 characters long without slashes:
if len(username.replace("/", "")) < USERNAME_MIN_LENGTH:
        return False

The last is to ensure that usernames are alphanumeric(isalnum()).

In the same vain, is_valid_password() uses django's validate_password to ensure that user passwords pass django's password validations.

is_valid_email() ensures that provided email addresses are not None, unique and follow normal email pattern (such as person@domain).

All the validate_*() are helper functions for real-time validations.

Let's use them in our views.py file.

# accounts > views.py
...
from django.http import JsonResponse
...
from . import utils
...

def validate_email(request):
    email = request.POST.get("email", None)
    validated_email = utils.validate_email(email)
    res = JsonResponse({"success": True, "msg": "Valid e-mail address"})
    if not validated_email["success"]:
        res = JsonResponse({"success": False, "msg": validated_email["reason"]})
    return res

def validate_username(request):
    username = request.POST.get("username", None).replace("/", "")
    validated_username = utils.validate_username(username)
    res = JsonResponse({"success": True, "msg": "Valid student number."})
    if not validated_username["success"]:
        res = JsonResponse({"success": False, "msg": validated_username["reason"]})
    return res

We are just utilizing our utils, right? After getting the values from the POST requests, they are passed in the functions we prepared before hand so as return some json data.

Add these to urls.py file:

urlpatterns = [
...
path("validate-username/", views.validate_username, name="validate_username"),
path("validate-email/", views.validate_email, name="validate_email"),
]

Let's wrap this section up by implementing the student registration view function.

from django.conf import settings
...
from django.contrib.sites.shortcuts import get_current_site
...
from . import tasks, utils
from .forms import LoginForm, StudentRegistrationForm
from .tokens import account_activation_token

...

def student_signup(request):
    form = StudentRegistrationForm(request.POST or None)
    if request.method == "POST":
        post_data = request.POST.copy()
        email = post_data.get("email")
        username = post_data.get("username").replace("/", "")
        password = post_data.get("password1")

        if utils.is_valid_email(email):
            user = get_user_model().objects.create(email=post_data.get("email"))

        if utils.is_valid_password(password, user) and utils.is_valid_username(username):
            user.set_password(password)
            user.username = username
            user.first_name = post_data.get("first_name")
            user.last_name = post_data.get("last_name")
            user.level = post_data.get("level")
            user.gender = post_data.get("gender")
            user.is_active = False
            user.is_student = True
            user.save()
            subject = "Please Activate Your Student Account"
            ctx = {
                "fullname": user.get_full_name(),
                "domain": str(get_current_site(request)),
                "uid": urlsafe_base64_encode(force_bytes(user.pk)),
                "token": account_activation_token.make_token(user),
            }

            if settings.DEBUG:
                tasks.send_email_message.delay(
                    subject=subject,
                    template_name="accounts/activation_request.txt",
                    user_id=user.id,
                    ctx=ctx,
                )
            else:
                tasks.send_email_message.delay(
                    subject=subject,
                    template_name="accounts/activation_request.html",
                    user_id=user.id,
                    ctx=ctx,
                )
            raw_password = password
            user = authenticate(username=username, password=raw_password)
            return JsonResponse(
                {
                    "success": True,
                    "msg": "Your account has been created! You need to verify your email address to be able to log in.",
                    "next": reverse("accounts:activation_sent"),
                }
            )
        else:
            get_user_model().objects.get(email=post_data.get("email")).delete()
            return JsonResponse(
                {
                    "success": False,
                    "msg": "Check your credentials: your password, username, and email! Ensure you adhere to all the specified measures.",
                }
            )
    context = {"page_title": "Student registration", "form": form}
    return render(request, "accounts/student_signup.html", context)

Reading through the code should clear some difficulties. Just a bunch of if statements to ensure all fields conform with the rules laid down before. We need to create tasks.py and token.py files. Go ahead and do that.

We will define their usefullness in the next part and proceed with the implementation of this software.

See ya!!!

Outro

23