🚀 Jump-start your Django projects with this template! Providing seamless JWT authentication, CRUD operations, and more - all built on Django Ninja. 🥷
from http import HTTPStatus

from api.views import item_router, user_auth_router, user_no_auth_router
from django.core.exceptions import (
    FieldError,
    ObjectDoesNotExist,
    PermissionDenied,
    ValidationError,
)
from ninja.errors import ValidationError as NinjaValidationError
from ninja_extra import NinjaExtraAPI
from ninja_jwt.authentication import JWTAuth
from ninja_jwt.controller import NinjaJWTDefaultController

api = NinjaExtraAPI(urls_namespace="api")
api.register_controllers(NinjaJWTDefaultController)


api.add_router("/users", user_no_auth_router, tags=["users"])
api.add_router("/users", user_auth_router, auth=JWTAuth(), tags=["users"])
api.add_router("/items", item_router, auth=JWTAuth(), tags=["items"])


@api.exception_handler(ObjectDoesNotExist)
def handle_object_does_not_exist(request, exc):
    return api.create_response(
        request,
        {"message": "ObjectDoesNotExist", "detail": str(exc)},
        status=HTTPStatus.NOT_FOUND,
    )


@api.exception_handler(PermissionDenied)
def handle_permission_error(request, exc: PermissionDenied):
    return api.create_response(
        request,
        {
            "message": "PermissionDenied",
            "detail": "You don't have the permission to access this resource.",
        },
        status=HTTPStatus.FORBIDDEN,
    )


@api.exception_handler(NinjaValidationError)
def handle_ninja_validation_error(request, exc: NinjaValidationError):
    mapped_msg = {error["loc"][-1]: error["msg"] for error in exc.errors}
    return api.create_response(
        request,
        data={"message": "NinjaValidationError", "detail": mapped_msg},
        status=HTTPStatus.BAD_REQUEST,
    )


@api.exception_handler(ValidationError)
def handle_validation_error(request, exc: ValidationError):
    status = HTTPStatus.BAD_REQUEST
    for field, errors in exc.error_dict.items():
        for error in errors:
            if error.code in ["unique", "unique_together"]:
                status = HTTPStatus.CONFLICT
    return api.create_response(
        request,
        data={"message": "ValidationError", "detail": exc.message_dict},
        status=status,
    )


@api.exception_handler(FieldError)
def handle_field_error(request, exc: FieldError):
    return api.create_response(
        request,
        data={"message": "FieldError", "detail": str(exc)},
        status=HTTPStatus.BAD_REQUEST,
    )

backend/api/api.py
Yacobolo/django-ninja-crud-template
from django.conf import settings
from django.db import models
from django.utils import timezone
from simple_history.models import HistoricalRecords

from api.utils.model_manager import ActiveManager


class Item(models.Model):
    name = models.CharField(max_length=100)
    description = models.TextField(blank=True, null=True)

    # Metadata
    created_at = models.DateTimeField(default=timezone.now)
    created_by = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        related_name="created_items",
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
    )
    updated_at = models.DateTimeField(auto_now=True)
    updated_by = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        related_name="updated_items",
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
    )
    deleted_at = models.DateTimeField(blank=True, null=True)
    is_active = models.BooleanField(default=True)
    is_deleted = models.BooleanField(default=False)

    history = HistoricalRecords(table_name="item_history")

    objects = ActiveManager()

    def __str__(self):
        return self.name

    class Meta:
        db_table = "item"
        indexes = [models.Index(fields=["id", "name"])]

backend/api/models/item.py
Yacobolo/django-ninja-crud-template
from .item import ItemIn, ItemOut
from .user import UserIn, UserOut

backend/api/schemas/__init__.py
Yacobolo/django-ninja-crud-template
# schemas.py
from datetime import datetime
from typing import Optional

from ninja import Schema


class ItemIn(Schema):
    name: str
    description: Optional[str] = None


class ItemOut(Schema):
    id: int
    name: str
    description: Optional[str]
    created_at: datetime
    created_by_id: Optional[int]
    updated_at: datetime
    updated_by_id: Optional[int]
    is_active: bool

backend/api/schemas/item.py
Yacobolo/django-ninja-crud-template
from ninja import Schema


class UserIn(Schema):
    username: str
    email: str
    password: str


class UserOut(Schema):
    id: int
    username: str
    email: str

backend/api/schemas/user.py
Yacobolo/django-ninja-crud-template
from .soft_delete import SoftDeleteModelView

backend/api/utils/crud_views/__init__.py
Yacobolo/django-ninja-crud-template
from http import HTTPStatus
from typing import Callable, List, Optional, Type

from django.db.models import Model
from django.http import HttpRequest
from ninja import Router
from ninja_crud.views import utils
from ninja_crud.views.abstract import AbstractModelView
from ninja_crud.views.types import PostDeleteHook, PreDeleteHook


class SoftDeleteModelView(AbstractModelView):
    def __init__(
        self,
        pre_delete: PreDeleteHook = None,
        post_delete: PostDeleteHook = None,
        decorators: List[Callable] = None,
        router_kwargs: Optional[dict] = None,
    ) -> None:
        super().__init__(decorators=decorators, router_kwargs=router_kwargs)
        self.pre_delete = pre_delete
        self.post_delete = post_delete

    def register_route(self, router: Router, model_class: Type[Model]) -> None:
        @router.delete(**self._get_default_router_kwargs(model_class))
        @utils.merge_decorators(self.decorators)
        def delete_model(request: HttpRequest, id: utils.get_id_type(model_class)):
            instance = model_class.objects.get(pk=id)

            if self.pre_delete is not None:
                self.pre_delete(request, instance)

            instance.is_deleted = True
            instance.save()

            if self.post_delete is not None:
                self.post_delete(request, id, instance)

            return HTTPStatus.NO_CONTENT, None

    def _get_default_router_kwargs(self, model_class: Type[Model]) -> dict:
        return dict(
            path=self.get_path(),
            response={HTTPStatus.NO_CONTENT: None},
            operation_id=self._get_operation_id(model_class),
            summary=self._get_summary(model_class),
        )

    def get_path(self) -> str:
        return "/{id}"

    @staticmethod
    def _get_operation_id(model_class: Type[Model]) -> str:
        return f"delete_{utils.to_snake_case(model_class.__name__)}"

    @staticmethod
    def _get_summary(model_class: Type[Model]) -> str:
        return f"Delete {model_class.__name__}"

backend/api/utils/crud_views/soft_delete.py
Yacobolo/django-ninja-crud-template
from django.db import models


class ActiveManager(models.Manager):
    def get_queryset(self):
        return super().get_queryset().filter(is_deleted=False)

backend/api/utils/model_manager.py
Yacobolo/django-ninja-crud-template
def set_created_updated_by_on_create(request, instance):
    """
    Set created_by and updated_by fields for the model instance during creation.

    :param request: HttpRequest object
    :param instance: Instance of the model being created
    """
    user = request.user
    instance.created_by = user
    instance.updated_by = user


def set_updated_by_on_update(request, old_instance, new_instance):
    """
    Set updated_by field for the model instance during update.

    :param request: HttpRequest object
    :param old_instance: Instance of the model before update
    :param new_instance: Instance of the model after update
    """
    user = request.user
    new_instance.updated_by = user

backend/api/utils/model_utils.py
Yacobolo/django-ninja-crud-template
from .item import router as item_router
from .user import auth_router as user_auth_router
from .user import no_auth_router as user_no_auth_router

backend/api/views/__init__.py
Yacobolo/django-ninja-crud-template
from api.models import Item
from api.schemas import ItemIn, ItemOut
from api.utils.crud_views import SoftDeleteModelView
from api.utils.model_utils import (
    set_created_updated_by_on_create,
    set_updated_by_on_update,
)
from ninja import Router
from ninja_crud.views import (
    CreateModelView,
    ListModelView,
    ModelViewSet,
    RetrieveModelView,
    UpdateModelView,
)

router = Router()


class ItemViewSet(ModelViewSet):
    model_class = Item

    # AbstractModelView subclasses can be used as-is
    list = ListModelView(output_schema=ItemOut)
    create = CreateModelView(
        input_schema=ItemIn,
        output_schema=ItemOut,
        pre_save=set_created_updated_by_on_create,
    )
    retrieve = RetrieveModelView(output_schema=ItemOut)
    update = UpdateModelView(
        input_schema=ItemIn,
        output_schema=ItemOut,
        pre_save=set_updated_by_on_update,
    )
    delete = SoftDeleteModelView()


# The register_routes method must be called to register the routes with the router
ItemViewSet.register_routes(router)

backend/api/views/item.py
Yacobolo/django-ninja-crud-template
from api.models.user import User
from api.schemas.user import UserIn, UserOut
from django.contrib.auth import get_user_model
from ninja import Router
from ninja_crud.views import (
    DeleteModelView,
    ListModelView,
    ModelViewSet,
    RetrieveModelView,
    UpdateModelView,
)

no_auth_router = Router()


@no_auth_router.post("/", response=UserOut)
def register_user(request, user_in: UserIn):
    user = get_user_model().objects.create_user(
        username=user_in.username, email=user_in.email, password=user_in.password
    )
    return user


auth_router = Router()


class UserViewSet(ModelViewSet):
    model_class = User

    # AbstractModelView subclasses can be used as-is
    list = ListModelView(output_schema=UserOut)
    retrieve = RetrieveModelView(output_schema=UserOut)
    update = UpdateModelView(input_schema=UserIn, output_schema=UserOut)
    delete = DeleteModelView()


# The register_routes method must be called to register the routes with the router
UserViewSet.register_routes(auth_router)


# @auth_router.get("/me")
# def get_current_user(request):
#     """
#     Get the current authenticated user.
#     """
#     user = request.user

#     if user.is_authenticated:
#         return UserOut(id=user.id, username=user.username, email=user.email)
#     else:
#         return {"detail": "Authentication credentials were not provided."}

backend/api/views/user.py
Yacobolo/django-ninja-crud-template
"""
Django settings for yourproject project.

Generated by 'django-admin startproject' using Django 4.2.4.

For more information on this file, see
https://docs.djangoproject.com/en/4.2/topics/settings/

For the full list of settings and their values, see
https://docs.djangoproject.com/en/4.2/ref/settings/
"""

from pathlib import Path

# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent


# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = "django-insecure-4%xy&*^e5-r=$(@83r)%vr3h6_d^p6(724(v5sqxgiy#er!#0t"

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True

ALLOWED_HOSTS = []


# Application definition

INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    "ninja_jwt",
    "ninja_extra",
    "api",
    "simple_history",
]

MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    "django.contrib.sessions.middleware.SessionMiddleware",
    "django.middleware.common.CommonMiddleware",
    "django.middleware.csrf.CsrfViewMiddleware",
    "django.contrib.auth.middleware.AuthenticationMiddleware",
    "django.contrib.messages.middleware.MessageMiddleware",
    "django.middleware.clickjacking.XFrameOptionsMiddleware",
    "simple_history.middleware.HistoryRequestMiddleware",
]

ROOT_URLCONF = "yourproject.urls"

TEMPLATES = [
    {
        "BACKEND": "django.template.backends.django.DjangoTemplates",
        "DIRS": [],
        "APP_DIRS": True,
        "OPTIONS": {
            "context_processors": [
                "django.template.context_processors.debug",
                "django.template.context_processors.request",
                "django.contrib.auth.context_processors.auth",
                "django.contrib.messages.context_processors.messages",
            ],
        },
    },
]

WSGI_APPLICATION = "yourproject.wsgi.application"


# Database
# https://docs.djangoproject.com/en/4.2/ref/settings/#databases

DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.sqlite3",
        "NAME": BASE_DIR / "db.sqlite3",
    }
}


# Password validation
# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators

AUTH_PASSWORD_VALIDATORS = [
    {
        "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
    },
    {
        "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
    },
    {
        "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
    },
    {
        "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
    },
]


# Internationalization
# https://docs.djangoproject.com/en/4.2/topics/i18n/

LANGUAGE_CODE = "en-us"

TIME_ZONE = "UTC"

USE_I18N = True

USE_TZ = True


# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.2/howto/static-files/

STATIC_URL = "static/"

# Default primary key field type
# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field

DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"

AUTH_USER_MODEL = "api.User"

# JWT settings
NINJA_JWT_SECRET = "your_secret_key"
NINJA_JWT_ALGORITHM = "HS256"
NINJA_JWT_ACCESS_TOKEN_LIFETIME = 3600  # 1 hour
NINJA_JWT_REFRESH_TOKEN_LIFETIME = 86400  # 1 day

backend/yourproject/settings.py
Yacobolo/django-ninja-crud-template
from api.api import api
from django.contrib import admin
from django.urls import path

urlpatterns = [
    path("admin/", admin.site.urls),
    path("api/", api.urls),
]

backend/yourproject/urls.py
Yacobolo/django-ninja-crud-template