django_real_time_validation
Django and Ajax: Robust authentication and authorization system with real-time form validations for web applications
In the previous part, we designed the database schema to address this part of the specification:
Build a multi-user authentication system. A user can either be a Student or a Lecturer ...
The source code to this point is hosted on github while the source code for the entire application is:
Django and Ajax: Robust authentication and authorization system with real-time form validations for web applications
The project is also live on heroku and can be accessed via this django-authentication-app.herokuapp.com
In this part, we'll take a tour of how the logic will be implemented. Some part of views.py
, urls.py
, forms.py
, and authentication.py
will be implemented.
Let's put our coding hart 👲 on and get our hands 🧰 dirty!
First off, we'll be using additional files as follows:
accounts/forms.py
: this holds everything form related.accounts/utils.py
: to avoid cluttering the views.py
file, helper functions will be domiciled here.accounts/authentication.py
: this houses the custom authentication backend we'll be using to enable signing in with both email address and username.To create the files, navigate to your terminal and run the following command:
┌──(sirneij@sirneij)-[~/Documents/Projects/Django/django_real_time_validation]
└─$[sirneij@sirneij django_real_time_validation]$ touch accounts/utils.py accounts/forms.py accounts/authentication.py
A section of the specification we are implementing says:
...Users can either login with any of Email/Password or Username/Password combination...
To do this, we need a custom authentication backend. Luckily, django gives us a pointer to how this can be done. Fire up you text editor and make accounts/authentication.py
look like this:
# accounts > authentication.py
from .models import User
class EmailAuthenticationBackend(object):
"""
Authenticate using an e-mail address.
"""
def authenticate(self, request, username=None, password=None):
try:
user = User.objects.get(email=username)
if user.check_password(password): # and user.is_active:
return user
return None
except User.DoesNotExist:
return None
def get_user(self, user_id):
try:
return User.objects.get(pk=user_id)
except User.DoesNotExist:
return None
We aint inheriting any built-in backend here but this still works. However, we still fall back to Django's default authentication backend which authenticates with username.
Though we have written this self-explanatory code snippet, it does nothing yet. To make it do something, we need to register it. Append the snippet below to your project's settings.py
file:
# authentication > settings.py
...
AUTHENTICATION_BACKENDS = [
"django.contrib.auth.backends.ModelBackend",
"accounts.authentication.EmailAuthenticationBackend", # our new authentication backend
]
...
Let's add our new User
model to django's admin page. Open up accounts/admin.py
and append the following:
# accounts > admin.py
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from .models import User
class CustomUserAdmin(UserAdmin):
model = User
readonly_fields = [
"date_joined",
]
actions = [
"activate_users",
]
list_display = (
"username",
"email",
"first_name",
"last_name",
"is_staff",
"is_student",
"is_lecturer",
)
def get_inline_instances(self, request, obj=None):
if not obj:
return list()
return super(CustomUserAdmin, self).get_inline_instances(request, obj)
def get_form(self, request, obj=None, **kwargs):
form = super().get_form(request, obj, **kwargs)
is_superuser = request.user.is_superuser
disabled_fields = set()
if not is_superuser:
disabled_fields |= {
"username",
"is_superuser",
}
# Prevent non-superusers from editing their own permissions
if not is_superuser and obj is not None and obj == request.user:
disabled_fields |= {
"is_staff",
"is_superuser",
"groups",
"user_permissions",
}
for f in disabled_fields:
if f in form.base_fields:
form.base_fields[f].disabled = True
return form
def activate_users(self, request, queryset):
cannot = queryset.filter(is_active=False).update(is_active=True)
self.message_user(request, "Activated {} users.".format(cannot))
activate_users.short_description = "Activate Users" # type: ignore
def get_actions(self, request):
actions = super().get_actions(request)
if not request.user.has_perm("auth.change_user"):
del actions["activate_users"]
return actions
admin.site.register(User, CustomUserAdmin)
We have set up custom user admin business logic. In the code, we added a custom action activate user
which allows a large number of users to be activated at once. This was implemented in case the registration flow we are planning fails and we want the superuser to be empowered with the ability to mass-activate users. We also hide a couple of fields from any user who has access to the admin page but not a superuser
. This is for security concerns. To learn more about this, Haki Benita's article is an awesome guide.
It's time to test our custom authentication
backend. First, we need a form to login users. Let's create it.
# accounts > forms.py
from django import forms
class LoginForm(forms.Form):
username = forms.CharField(widget=forms.TextInput(attrs={"placeholder": "Username or Email"}))
password = forms.CharField(widget=forms.PasswordInput(attrs={"placeholder": "Password"}))
def __init__(self, *args, **kwargs):
super(LoginForm, self).__init__(*args, **kwargs)
for visible in self.visible_fields():
visible.field.widget.attrs["class"] = "validate"
It is a very simple form with two fields: username
and password
. However, the username
field also accommodates email
addresses. This is to conform with our specification. The __init__
dunder method applies class=validate
to all the visible fields in the form. It is a nice shortcut mostly when you are working with ModelForms
. This validate
class is available in materialize css
. The next agender is to use this form in the views.py
file.
# accounts > views.py
from django.contrib import messages
from django.contrib.auth import authenticate, login
from django.shortcuts import redirect, render
from django.urls.base import reverse
from .forms import LoginForm
...
def login_user(request):
form = LoginForm(request.POST or None)
msg = "Enter your credentials"
if request.method == "POST":
if form.is_valid():
username = form.cleaned_data.get("username").replace("/", "")
password = form.cleaned_data.get("password")
user = authenticate(username=username, password=password)
if user is not None:
if user.is_active:
login(request, user, backend="accounts.authentication.EmailAuthenticationBackend")
messages.success(request, f"Login successful!")
if "next" in request.POST:
return redirect(request.POST.get("next"))
else:
return redirect("accounts:index")
else:
messages.error(
request,
f"Login unsuccessful! Your account has not been activated. Activate your account via {reverse('accounts:resend_email')}",
)
msg = "Inactive account details"
else:
messages.error(request, f"No user with the provided details exists in our system.")
else:
messages.error(request, f"Error validating the form")
msg = "Error validating the form"
context = {
"form": form,
"page_title": "Login in",
"msg": msg,
}
return render(request, "accounts/login.html", context)
It is a basic authentication logic. Some pointers are removing all forward slashes , /
from the inputted username, in the case of students, and using our custom authentication backend:
...
login(request, user, backend="accounts.authentication.EmailAuthenticationBackend")
...
to login users in. We also covered the part of the specification that says:
Until confirmation, no user is allowed to log in.
Though, by default, you can not login if is_active=False
but since we are using custom authentication
backend, I feel we should enforce that. We could have done this earlier on in the authentication
backend code. Next, we check whether there is a page we need to redirect to by checking the content of next
. We will put this in our template soon. It is a nice way to redirect users back to wherever they wanted to visit before being asked to login.
Let's add this and django's built-in logout
view to our urls.py
file.
# accounts > urls.py
from django.contrib.auth import views as auth_views
...
urlpatterns = [
...
path("login", views.login_user, name="login"),
path("logout/", auth_views.LogoutView.as_view(), name="logout"),
]
By extension, let's register this in our settings.py
file too.
# accounts > settings.py
...
AUTH_USER_MODEL = "accounts.User"
LOGIN_URL = "accounts:login"
LOGOUT_URL = "accounts:logout"
LOGOUT_REDIRECT_URL = "accounts:index"
...
We always want to go back to home page when we logout.
FInally, it's time to render it out.
{% extends "base.html" %}
<!--static-->
{% load static %}
<!--title-->
{% block title %}{{page_title}}{% endblock %}
<!--content-->
{% block content%}
<h4 id="signup-text">Welcome back</h4>
<div class="form-container">
<!-- <h5 class="auth-header">Assignment Management System</h5>-->
<div class="signin-form">
<form method="POST" action="" id="loginForm">
{% csrf_token %}
<!---->
<h5 style="text-align: ceneter">{{msg}}</h5>
<div class="row">
{% for field in form %}
<div class="input-field col s12">
{% if forloop.counter == 1 %}
<i class="material-icons prefix">email</i>
{% elif forloop.counter == 2 %}
<i class="material-icons prefix">vpn_key</i>
{% endif %}
<label for="id_{{field.label|lower}}"> {{field.label}}* </label>
{{ field }}
<!---->
{% if field.errors %}
<span class="helper-text email-error">{{field.errors}}</span>
{% endif %}
</div>
{% endfor %}
</div>
<!---->
{% if request.GET.next %}
<input type="hidden" name="next" value="{{request.GET.next}}" />
{% endif %}
<button
class="btn waves-effect waves-light btn-large"
type="submit"
name="login"
id="loginBtn"
>
Log in
<i class="material-icons right">send</i>
</button>
</form>
<ul>
<li class="forgot-password-link">
<a href="#"> Forgot password?</a>
</li>
</ul>
</div>
<div class="signup-illustration">
<img
src="{% static 'img/sign-up-illustration.svg' %}"
alt="Sign in illustration"
/>
</div>
</div>
{% endblock %}
It is a basic materialize css form with icons. Since we have only two fields, username/email
and password
, we use if
statement to check the forloop
counter and put icons
appropriately. Noticed this line?:
{% if request.GET.next %}
<input type="hidden" name="next" value="{{request.GET.next}}" />
{% endif %}
This is what saves the next
field we discussed previously. It is a hidden input since we don't want users to see it's content, just for reference.
To incept the real-time form validation we've been clamouring for, let's add a bit of JavaScript to this form. At first, we want the Log in
button to be disabled until users type in both the username or email
and password
. That's enough for now.
Append this code to templates/accounts/login.html
file:
<!---->
{% block js %}
<script>
const loginForm = document.getElementById("loginForm");
const formElements = document.querySelectorAll("#loginForm input");
loginForm.addEventListener("keyup", (event) => {
let empty = false;
formElements.forEach((element) => {
if (element.value === "") {
empty = true;
}
});
if (empty) {
$("#loginBtn").addClass("disabled");
} else {
$("#loginBtn").removeClass("disabled");
}
});
</script>
{% endblock js %}
It simply listens to keyup
events in any of the form's input elements. If any is empty, the button remains disabled, else? Enabled! Simple huh 😎!
Modify the button to be disabled by default.
...
<button class="btn waves-effect waves-light btn-large disabled"
type="submit"
name="login"
id="loginBtn"
>
Log in
<i class="material-icons right">send</i>
</button>
...
We have already created a js
block at the bottom of templates/base.html
file
Now, update your templates/includes/_header.html
so we can have easy navigation for both mobile and desktop portions.
...
<li><a href="{% url 'accounts:logout' %}">Logout</a></li>
...
<li><a href="{% url 'accounts:login' %}">Login</a></li>
...
Can we test it out now? Because I can't wait 💃🕺.
Damn! It is appealing 🤗... Create a superuser account and test it out with either Email or username
and password
.
You want the code to this end? Get it on github
Let's end it here, it's becoming unbearably too long 😌. See ya 👋 🚶!!!
Enjoyed this article, consider contacting me for a job, something worthwhile or buying a coffee ☕.