Django and Ajax: Robust authentication and authorization system with real-time form validations for web applications - 3
Tags: webdev, django, python, tutorial
Published: 2021-07-26 23:39:45



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 &mdash; to adhere to the specification &mdash; 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][1] while the source code for the entire application is:

{% github Sirneij/django_real_time_validation %}

## Step 5: Student registration form

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

```python
# 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][2]. `UserCreationForm` is simply a [ModelForm][3] with three fields inherited from user model &mdash; username, password1 and password2 &mdash; 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:

```python
# 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:
  ```python
  ...
  if get_user_model().objects.filter(username=username).exists():
        return False
  ```
- Username for students must start with `CPE`:
  ```python
  if not username.lower().startswith("cpe"):
        return False
  ```
- Users' username must not be less than 9 characters long without slashes:

  ```python
  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`][4] 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.

```python
# 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:

```python
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.

```python
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

Enjoyed this article, consider [contacting me for a job, something worthwhile or buying a coffee ☕](mailto:sirneij@gmail.com).

[1]: https://github.com/Sirneij/django_real_time_validation/tree/6ce9af1ca64cd154d30f7db2ce9dfd861cd31546 "Full code for the second part"
[2]: https://docs.djangoproject.com/en/3.2/topics/auth/default/#built-in-auth-forms "Built-in forms"
[3]: https://docs.djangoproject.com/en/3.2/topics/forms/modelforms/#django.forms.ModelForm "ModelForms"
[4]: https://docs.djangoproject.com/en/3.2/topics/auth/passwords/#integrating-validation "Integrating validation"