In the previous part, we went through setting up a 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 the 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 log in.
Source code
The source code to this point is hosted on github while the source code for the entire application is:
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 too 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 patterns (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 beforehand so as to 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 to the rules laid down before. We need to create tasks.py
and token.py
files. Go ahead and do that.
We will define their usefulness in the next part and proceed with the implementation of this software.
See ya!!!
Outro
Enjoyed this article? I'm a Software Engineer and Technical Writer actively seeking new opportunities, particularly in areas related to web security, finance, healthcare, and education. If you think my expertise aligns with your team's needs, let's chat! You can find me on LinkedIn and Twitter.
If you found this article valuable, consider sharing it with your network to help spread the knowledge!
Top comments (0)