DEV Community

Cover image for Django accounts management app (1), registration and activation
Saad Alkentar
Saad Alkentar

Posted on

Django accounts management app (1), registration and activation

What to expect from this article?

We have created the project skeletal structure in the previous article, this article will build on it. it will cover

  • Accounts database structure, including users and verification code.
  • Models serializers.
  • Account Views for account registration, activation. next article should cover the rest of the views, such as logging in, refreshing tokens, changing password, forgetting password and re-sending code.

I'll try to cover as many details as possible without boring you, but I still expect you to be familiar with some aspects of Python and Django.

the final version of the source code can be found on https://github.com/saad4software/alive-diary-backend

Series order

Check previous articles if interested!

  1. AI Project from Scratch, The Idea, Alive Diary
  2. Prove it is feasible with Google AI Studio
  3. Django API Project Setup
  4. Django accounts management app (1), registration and activation (You are here 📍)

Accounts app setup

let's create serializers file in the app

from rest_framework import serializers
from app_account.models import *
Enter fullscreen mode Exit fullscreen mode

app_account/serializers.py

and the urls file

from django.urls import path, include
from .views import *

urlpatterns = [

]
Enter fullscreen mode Exit fullscreen mode

app_account/urls.py

finally, let's connect the app urls to the project urls by editing projects urls file as

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/account/', include('app_account.urls')),

]
Enter fullscreen mode Exit fullscreen mode

alive_diary/urls.py

Now we can call any accounts url with the prefix "api/account/"

The Models

the main model for accounts app is the User model of course

from django.db import models
from django.contrib.auth.models import AbstractUser
from datetime import timedelta, datetime

class User(AbstractUser):
    userTypes = (
        ('A', 'Admin'),
        ('C', 'Client'),
    )

    role = models.CharField(max_length=1, choices=userTypes, default="C")

    hobbies = models.CharField(max_length=255, null=True, blank=True)
    job = models.CharField(max_length=100, null=True, blank=True)
    bio = models.TextField(null=True, blank=True)

    country_code = models.CharField(max_length=10, null=True, blank=True)
    expiration_date = models.DateTimeField(default=datetime.now()+timedelta(days=30))
Enter fullscreen mode Exit fullscreen mode

app_account/models.py

Usually, it is better to keep the User model as simple as possible and move other details to a Profile model with a one-to-one relationship with the User, but to simplify things, I'll add the required user info directly to the User model this time.

We are inheriting from AbstractUser model, AbstractUser includes multiple fields

class AbstractUser(AbstractBaseUser, PermissionsMixin):
    username = models.CharField(...)
    first_name = models.CharField(...)
    last_name = models.CharField(...)
    email = models.EmailField(...)
    is_staff = models.BooleanField(...)
    is_active = models.BooleanField(...),
    date_joined = models.DateTimeField(...)

Enter fullscreen mode Exit fullscreen mode

the most important ones are:

  • username will be used for logging in.
  • is_active will be used to prevent unverified accounts from logging in.
  • is_staff will distinguish admin (with value true) from normal users.

We have also added multiple fields for this project user which are

  • role to distinguish admin from client accounts, we can use is_staff for this simple project since we only have two roles, but a larger project can have more than 2 roles, making this field essential for permissions handling.
  • hobbies, job, bio Knowing more about the user can help build a better reflection, therefore we are asking for hobbies, job, and how the user describes him/her self.
  • country_code for statistics
  • expiration_date for subscription-based expiration date.

We also need a Verification code model to keep and track verification codes for account activation, forgetting passwords, and resending codes.

import random

class VerificationCode(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    code = models.CharField(max_length=6, default=random.randint(111111, 999999))
    email = models.EmailField()
    expiration_date = models.DateTimeField(default=datetime.now()+timedelta(days=1))

    def __str__(self):
        return self.user.username
Enter fullscreen mode Exit fullscreen mode

app_account/models.py

It connects with the User model and generates a random value of a 6-digit code number. it also has an expiration time of 24 hours. we have also email filed in case the user wants to validate multiple email addresses, it is rare and can be removed for this app. Let's move to serializers next.

The Registration API

let's start with the serializer

from rest_framework import serializers
from app_account.models import *
from django.contrib.auth import get_user_model
from common.utils import is_valid_email


class RegisterSerializer(serializers.ModelSerializer):
    password1 = serializers.CharField(write_only=True)
    password2 = serializers.CharField(write_only=True)

    def validate_username(self, value):
        if not is_valid_email(value):
            raise serializers.ValidationError("invalid_email")
        return value

    def validate(self, data):
        if data['password1'] != data['password2']:
            raise serializers.ValidationError("password_dont_match")

        data['password'] = data['password1']

        data.pop('password1', None)
        data.pop('password2', None)

        return data

    class Meta:
        model = get_user_model()
        fields = [
            'id',
            'email',
            'password1',
            'password2',
            'first_name',
            'last_name',
            'country_code',
            'username',
        ]
        read_only_fields = ['id',]

Enter fullscreen mode Exit fullscreen mode

app_account/serializers.py

We are using ModelSerializer, from Django rest framework. we selected the user model get_user_model() in class Meta and a list of serialized fields.

We have added two additional fields to the model serializer, password1, and password2. To validate they have the same value, we have overwritten the validate method. and to enforce using valid email as a username, we have added a field validator for username field.
the is_valid_email function should look somewhat like this

import re

def is_valid_email(email):
    regex = r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,7}\b'
    return email and isinstance(email, str) and re.fullmatch(regex, email)

Enter fullscreen mode Exit fullscreen mode

common/utils.py

Personally, I don't like regular expressions, I've never got the grasp of them, but they seem to be the best way to validate emails. If you got a better way, please share it with us.

Since our new fields password1 and password2 don't belong to the original user model, we removed them from the data dictionary and added the password field in order to use serializer data directly to create a new user.

Serializer's best practices dilemma

  • Should serializers use database queries and edit databases?
  • or should they only make basic field type validation (string, integer, ...)?

There is no clear answer actually, for instance, Django Rest framework model serializers seem to make queries for unique fields, like the serializer error we got when tried to create a use with the same name, it was generated by the serializer, not the view.
The Create, Save, Update methods writes values to the database.
Yet, accessing the database only in views seems to align more with Separation of Concerns and Flexibility.

What do you think is better?

I've read a lot about keeping things separated, even separating database queries from database updating methods. so let's try doing that. creating the AccountActivateView in the views.py file should look like.

In our case, we can overwrite the create method for RegisterSerializer in order to create a new user and validation code instants, and even sending the verification code from the serializer.

But instead, I'll keep the model related operations in the views file

Let's move to the registration view

from django.shortcuts import render
from rest_framework import generics, status
from .serializers import *
from rest_framework.renderers import BrowsableAPIRenderer, JSONRenderer
from django.core.mail import send_mail
from django.conf import settings

class AccountRegisterView(generics.CreateAPIView):
    permission_classes = ()

    queryset = get_user_model().objects.all()
    serializer_class = RegisterSerializer
    renderer_classes = [BrowsableAPIRenderer, CustomRenderer]

    def perform_create(self, serializer):
        user = get_user_model().objects.create_user(**serializer.validated_data, is_active=False)

        # email verification code, assumed username is email!
        code = VerificationCode(user=user, email=user.username)
        code.save()

        send_mail(
            'Welcome to Alive Diary! 🚀',

            f"""
            Dear {user.first_name} {user.last_name},

            Welcome aboard! 🎉                                                                                                                                 

            Your activation code is {code.code}

            Best regards,
            Alive Diary team with ❤️

            """
            ,
            f'AliveDiary<{settings.EMAIL_SENDER}>',
            [user.username],
            fail_silently=False,
        )



Enter fullscreen mode Exit fullscreen mode

app_account/views.py

We are using CreatAPIView from the rest framework, it accepts POST requests with the schema of serializer_class, the BrowsableAPIRenderer build a web interface for this API and the JSONRenderer is responsible for building the JSON response.

Overwriting the perform_create method allows us control the user creation mechanism, we are creating the user instant, making sure the is_active field is set to False, then creating the verification code instant that is connected to the new user model, and finally sending an email with the verification code to the user.

Sending the email requires the correct configuration for email fields in the setting file, please let me know if you have issues in this particular point to create a separate article for it

Finally, let's add the API url

from django.urls import path, include
from .views import *

urlpatterns = [
    path('register/', AccountRegisterView.as_view()),
]
Enter fullscreen mode Exit fullscreen mode

app_account/urls.py

Nice, let's try it out

python manage.py makemigrations
python manage.py migrate
python manage.py runserver 0.0.0.0:8555
Enter fullscreen mode Exit fullscreen mode

opening http://localhost:8555/api/account/register, we should be able to see something like this (it is due to BrowsableAPIRenderer)

register BrowsableAPIRenderer

the required fields are username, password1 and password2, we expect the email to be used as username.
it looks okay, it created a user model with a verification code model connected to it (used SqlBrowser to open the SQLite db file). But the default response looks like this with the status 201.

{
    "country_code": null,
    "username": "test3@gmail.com"
}
Enter fullscreen mode Exit fullscreen mode

I Prefer all responses to have this schema

{
    "status": "success",
    "code": 200,
    "data": {},
    "message": None
}
Enter fullscreen mode Exit fullscreen mode
  • status should be either "success" or "error"
  • code is the response status code
  • data is the actual response data
  • message should contain error text or any other message

But how to do so?
The best way is by implementing a custom JSON renderer function. let's do it

from rest_framework.renderers import JSONRenderer

class CustomRenderer(JSONRenderer):
    def render(self, data, accepted_media_type=None, renderer_context=None):
        status_code = renderer_context['response'].status_code
        response = {
            "status": "success",
            "code": status_code,
            "data": data,
            "message": None
        }
        if not str(status_code).startswith('2'):
            response["status"] = "error"
            response["data"] = None

            if 'detail' in data:
                response["message"] = data["detail"]
            else:
                response['message'] = dict2string(data)

        return super(CustomRenderer, self).render(response, accepted_media_type, renderer_context)

def dict2string(data):
    msg = ""
    for key in data.keys():
        if key == "non_field_errors":
            msg += str(data[key][0]) + "\n\r"
        else:
            msg += key + ": " + str(data[key][0]) + "\n\r"
    return msg

Enter fullscreen mode Exit fullscreen mode

common/utils.py

We inherited from JSONRenderer and overwritten the render method. serializer errors.

  • started with reading the response status code, and putting it in the code field
  • and we moved the actual response data to the data field in our response schema
  • to distinguish errors from successful responses, we check the status code. If the status code is in the 200 family, it is a successful response
  • if not, it is an error. so we change the status to "error", and extract the error message from the response details field (if available).
  • serializer's validation errors don't have details fields, it is a dictionary indicating an error message (value) for each field name (key), so we created a small function dict2string to convert it to a simple string. I think it can be further improved, can you help with it?

that is it for the response schema, let's try using it now!
in views.py add our custom renderer to the register view class

from common.utils import CustomRenderer


class AccountRegisterView(generics.CreateAPIView):
    permission_classes = ()

    queryset = get_user_model().objects.all()
    serializer_class = RegisterSerializer
    renderer_classes = [BrowsableAPIRenderer, CustomRenderer]

    def perform_create(self, serializer):
        serializer.save()
Enter fullscreen mode Exit fullscreen mode

app_account/views.py

running the server, and opening http://localhost:8555/api/account/register/ shows the difference directly

Method not allowed error

we can see our schema in the error message 🤩, cool, let's try registering a new user, I'll call it "test5@gmail.com"

Register success response

looking great, now let's test the serializer validation error, we will try to register the same user again

Serializer error response

Wonderful, this is a validation error response, it was serialized as a field:message
what comes after registration? it is validation
register -> confirm email -> login -> whatever

The Activation API

We want to check if the user got the activation code we sent during registration or not, if the user sends the right code, we will activate their account, if not, we will ask them to check for it again, or maybe resend the code (another API for later)
Similar to the registration API-creating process, let's start with the serializer

class ActivateSerializer(serializers.Serializer):
    username = serializers.CharField(required=True)
    code = serializers.CharField(required=True)

Enter fullscreen mode Exit fullscreen mode

This is not related to a certain database model, so we are inheriting from the generic Serializer, notice that serializers are similar to forms, so we set the fields and their validation rules.
We are using two string fields (CharField), both are required, username which is the user email address, and code.

from rest_framework.views import APIView
from rest_framework.exceptions import APIException
from rest_framework.response import Response


class AccountActivateView(APIView):
    permission_classes = ()
    renderer_classes = [CustomRenderer, BrowsableAPIRenderer]

    def post(self, request, *args, **kwargs):
        serializer = ActivateSerializer(data=request.data)

        if not serializer.is_valid():
            raise APIException(serializer.errors)

        verification_query = VerificationCode.objects.filter(
            user__username=serializer.validated_data.get("username"), 
            email=serializer.validated_data.get("username"),
            code=serializer.validated_data.get("code"),
        ).order_by('-id')

        if not verification_query.exists():
            raise APIException("invalid_code")

        user = get_user_model().objects.filter(username=serializer.validated_data.get("username")).first()
        user.is_active=True
        user.save()

        verification_query.delete()

        return Response("success")

Enter fullscreen mode Exit fullscreen mode

app_account/views.py

Since we are using a custom API view, we are inheriting from APIView, it offers 5 functions (get, post, put, delete, and patch). We are de-serializing request data from POST requests, and validating its type, then making a query to find if the provided data exists or not, if it exists, we activate the user and remove the code object from its table. if not we send an error message saying it is an "invalid_code". finally, the URLs file should be updated to include this view's URL

from django.urls import path, include
from .views import *

urlpatterns = [
    path('register/', AccountRegisterView.as_view()),
    path('activate/', AccountActivateView.as_view()),

]
Enter fullscreen mode Exit fullscreen mode

app_account/urls.py

Now we can open the URL http://localhost:8555/api/account/activate/, we are using a custom API view, so it doesn't get the required field

Invalid code exception

We can get the code from the database (for testing purposes). The request should look like

{
    "username":"test5@gmail.com",
    "code":"123456"
}
Enter fullscreen mode Exit fullscreen mode

If everything went successfully, the response should look like

{
    "status": "success",
    "code": 200,
    "data": "success",
    "message": null
}
Enter fullscreen mode Exit fullscreen mode

That's it
Let's wrap it up! I know we haven't log in yet, but it became a very long article indeed, let's continue in the next article

Stay tuned 😎

Top comments (0)