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!
- AI Project from Scratch, The Idea, Alive Diary
- Prove it is feasible with Google AI Studio
- Django API Project Setup
- 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 *
app_account/serializers.py
and the urls file
from django.urls import path, include
from .views import *
urlpatterns = [
]
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')),
]
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))
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(...)
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 useis_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
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',]
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)
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,
)
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()),
]
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
opening http://localhost:8555/api/account/register, we should be able to see something like this (it is due to 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"
}
I Prefer all responses to have this schema
{
"status": "success",
"code": 200,
"data": {},
"message": None
}
-
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
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 functiondict2string
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()
app_account/views.py
running the server, and opening http://localhost:8555/api/account/register/
shows the difference directly
we can see our schema in the error message 🤩, cool, let's try registering a new user, I'll call it "test5@gmail.com"
looking great, now let's test the serializer validation error, we will try to register the same user again
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)
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")
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()),
]
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
We can get the code from the database (for testing purposes). The request should look like
{
"username":"test5@gmail.com",
"code":"123456"
}
If everything went successfully, the response should look like
{
"status": "success",
"code": 200,
"data": "success",
"message": null
}
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)