News

April 10, 2020

Creating a scalable Django project with multiple user types using using proxy models and Django-ninja, by Oluwafemi ‘Femiir’ Ojo

Some kinds of projects may have authentication requirements for which Django’s built-in User model is not always appropriate.

Often times you have to create a project that has multiple user types models, for example, a project that has business users and client users, Doctors, Nurses and Patient, or even a School administration system where you have Students, Teachers and Admin etc.

This is not an uncommon requirements in projects and it is important to know how to implement this in.

Django allows you to override the default user model by providing a value for the AUTH_USER_MODEL setting that references a custom model

There are several ways to implement this in Django – Ceate a single user model with a user_type field that defines the type of user – Use Boolean Fields – Concrete models

Dare i say Django comes shipped with an authentication system that covers most average system requirements. Knowing this allows you to capitalize on this

Django has a way to handle this using proxy models. Proxy models allow you to create a new model with the same fields as an existing model. This is useful when you want to change the behavior of the original model, but don’t want to change the original model itself. Proxy models are created using the Meta option proxy = True.

In this tutorial, we will create a Django project that has two user types, business users, and client users. We will use proxy models to create the two user types and django-ninja to create an API for the project.

Prerequisites – Basic knowledge of Django – Basic knowledge of RESTful API’s – Virtual Enviroments

Create env with tool of your choice.

conda is used for this tutorial. Feel free to use any env manager of choice

conda create -n multipleuser

Activate the model

conda activate multipleuser

Install the packages

pip install Django django-ninja

start a django project

django-admin startproject config

Start an app

django-admin startapp accounts

Create custom user

The easiest way to construct a compliant custom user model is to inherit from AbstractBaseUser. AbstractBaseUser provides the core implementation of a user model, including hashed passwords and tokenized password resets.

AbstractUser: Use this option if you are happy with the existing fields on the user model and just want to remove the username field.

AbstractBaseUser: Use this option if you want to start from scratch by creating your own, completely new user model.

we choose AbstractUser for simplicity.

To read more about this topic check out the following – scoops of django by Audrey Greenfeld and Daniel Roy Greenfeld – https://testdriven.io/blog/django-custom-user-model/ – https://docs.djangoproject.com/en/5.0/topics/auth/customizing/

Approach to this design

I like the make my models as flexible as possible which sees me trying to reduce the amount of hard coding i have to do.So i create a Roles model that will allow for as many user types to be created.What this allows me to do is link this model to my user table in any requirements my system needs either as – foriegn keys(roles can be updated)- One to one(user can be of specific type)- Many to many(user can have multiple roles)

# accounts/models.py
from django.db import models
from django.contrib.auth.models import AbstractUser
from .managers import BusinessManager, ClientManager


# Create your models here.
class TimeStampedModel(models.Model):
    created = models.DateTimeField(auto_now_add=True)
    modified = models.DateTimeField(auto_now=True)

    class Meta:
        abstract = True

always good to create a Timestamped model where other models can inherit from

class Roles(TimeStampedModel):
    name = models.CharField(max_length=20)
    description = models.TextField()

    class Meta:
        verbose_name = ‘Role’
        verbose_name_plural = ‘Roles’
        db_table = ‘roles’

    def __str__(self):
        return self.name


class User(AbstractUser):
    nickname = models.CharField(max_length=20, blank=True)
    user_role = models.ForeignKey(
        Roles, on_delete=models.CASCADE, null=True, blank=True
    )


class Business(User):
    objects = BusinessManager()

    def save(self, *args, **kwargs):
        if not self.user_role:
            self.user_role, created = Roles.objects.get_or_create(
                name=’Business’, description=’Business User Account’
            )
        super().save(*args, **kwargs)

    class Meta:
        proxy = True


class Client(User):
    objects = ClientManager()

    def save(self, *args, **kwargs):
        if not self.user_role:
            self.user_role, created = Roles.objects.get_or_create(
                name=’Client’, description=’Client User Account’
            )
        super().save(*args, **kwargs)

    class Meta:
        proxy = True

create the manager in accounts/managers.py

# accounts/managers.py
from django.db import models
from django.db.models.query import QuerySet


class BusinessManager(models.Manager):
    def get_queryset(self) -> QuerySet:
        return super().get_queryset().filter(user_role__name=’Business’)


class ClientManager(models.Manager):
    def get_queryset(self) -> QuerySet:
        return super().get_queryset().filter(user_role__name=’Client’)

Dont forget to point to the custom user in settings.py AUTH_USER_MODEL = ‘accounts.User’

# accounts/admin.py
from django.contrib.auth.admin import UserAdmin
from django.contrib import admin
from .models import User, Roles, Business, Client
from django.contrib.auth.admin import UserAdmin

# Register your models here.
admin.site.register(User, UserAdmin)
admin.site.register(Roles)
admin.site.register(Business)
admin.site.register(Client)

Now is a good time to make migrations and migrate our user model

python manage.py makemigrations
python manage.py migrate

Now you have yourself a pluggable user app you can use for all your needs.

create the super user

python manage.py createsuperuser

create the api app

You can call me the Django-ninja evangelist/disciple…I love love love the project and i try to introduce it any way or shape i can. Nothing against OG DjangoRestFramework.

you can should definitely give the project a try

django-admin startapp api

register the new app in config/settings.py

# config/settings.py


INSTALLED_APPS = [
    …
    ‘api.apps.ApiConfig’,
    …
]

Create a schema, router, and api scripts

touch api/schema.py
touch api/routers.py
touch api/scripts.py

Schema in django-ninja is what serializers are to DjangoRestFramework

Router in django-ninja is where I configure my URL’s

api can be seen as views

you can delete the api/admin, api/views, api/models ### Schema Schema converts django ORM to native pydantic types which gives you quick field validation out of the box. eg Enums, email, IPAddress, URLs, JSON,.It also gives you the ability to validate Fields, Nested models, and even create custom fields.

# api/schema.py
from typing import Optional
from ninja import ModelSchema, Schema
from accounts.models import User, Roles

class RolesSchema(ModelSchema):
    class Meta:
        model = Roles
        fields = [
            ‘id’,
            ‘name’,
            ‘description’,
        ]

class UserRegisterSchema(ModelSchema):
    user_role: str
    class Meta:
        model = User
        fields = [
            ‘username’,
            ‘nickname’,
            ‘password’,
            ‘user_role’,
        ]

# a response schema
class UserDetailSchema(Schema):
    id: int
    username: str
    nickname: str
    user_role: Optional[RolesSchema] = None  

Routers

If you are familiar with djangorestframework and understand viewsets and routers, then you will understand the concept of routers in django-ninja.

# api/routers.py
from ninja import NinjaAPI
from .api import router as user_router

api = NinjaAPI()

api.add_router(‘/user’, user_router)

API

This is where the magic happens. This is where you define your views and the logic that goes with it. we design the api to create a user and list users based on the user type.

# api/api.py
from ninja import Router
from .schema import UserRegisterSchema, UserDetailSchema
from accounts.models import Business, Client, User
from typing import List, Union

router = Router()
# Create your views here.
@router.post(‘create_user’, response=UserDetailSchema)
def create_user(request, payload: UserRegisterSchema):
    payload_dict = payload.dict()
    user_role = payload_dict.pop(‘user_role’)
    password = payload_dict.pop(‘password’)

    if user_role == ‘business’:
        user = Business(**payload_dict)
    elif user_role == ‘client’:
        user = Client(**payload_dict)
   
    # Hash the password
    user.set_password(password)
    user.save()
    return user


@router.get(”, response=Union[List[UserDetailSchema], dict])
def list_users(request, user_type: str = ‘all’):
    if user_type == ‘business’:
        queryset = Business.objects.all()
        total_user = f’Total business users: {queryset.count()}’
    elif user_type == ‘client’:
        queryset = Client.objects.all()
        total_user = f’Total client users: {queryset.count()}’
    else:
        queryset = User.objects.all()
        total_user = f’Total users[Business/Client]: {queryset.count()}’

    serialized_data = []
    for user in queryset:
        user_dict = UserDetailSchema.from_orm(user).dict()

        # Check if user_role is None and if the user is a superuser
        if user_dict[‘user_role’] is None and user.is_superuser:
            user_dict[‘user_role’] = {
                ‘id’: None,
                ‘name’: ‘Superuser’,
                ‘description’: ‘Superuser Account’,
            }

        serialized_data.append(user_dict)

    return {‘total_user’: total_user, ‘users’: serialized_data}

Notice how you need only one endpoint to create users of different types. This is the power of proxy models and the managers we created earlier.

# config/urls.py
from django.contrib import admin
from django.urls import path
from api import routers  # import routers from api

urlpatterns = [
    path(‘admin/’, admin.site.urls),
]
urlpatterns += [
    path(‘api/’, routers.api.urls),
]

You can run the server python.manage.py runserver and test the api endpoints by navigating to localhost:8000/api/docs.

Ninja out of the box provides documentation that allows you to test your endpoints. You can also test the endpoints using postman or any other API testing tool of your choice.

you can clone this project from github and run the project to see how it works.

At the end of the project your folder structure should look like

├── config
│   ├── asgi.py
│   ├── __init__.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── accounts
│   ├── migrations
│   │   ├── __init__.py
│   │   └── 0001_initial.py
│   ├── models.py
│   ├── __init__.py
│   ├── apps.py
│   ├── admin.py
│   ├── tests.py
│   ├── views.py
│   └── managers.py
├── api
│   ├── migrations
│   │   ├── __init__.py
│   ├── __init__.py
│   ├── apps.py
│   ├── api.py
│   ├── tests.py
│   ├── routers.py
│   └── schema.py
├── manage.py
├── db.sqlite3

To move this project to production you want to consider looking into the following suggestions

  • managing secrets, keys and passwords
  • using something like hacksoft django project style-guide or cookiecutter for managing consistent best practice
  • use a production ready database like postgres
  • containerization
  • adding extra fields to the user roles

This project focus on using ninja and django to develop a multiple user system that is scalable

I hope you enjoyed this tutorial. If you have any questions or suggestions, feel free to reach out to me on Twitter or LinkedIn. I would love to hear from you.

Oluwafemi “Femiir” Ojo is an enthusiastic, diligent, confident, self-motivated, and highly skilled DevOps engineer and Python developer with a passion for creating efficient and scalable solutions. Seeking opportunities to leverage expertise in both areas to contribute to innovative projects.