Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ jobs:
- name: Basic
args: "ci_tool=Gitlab"
- name: Celery & DRF
args: "use_celery=y use_drf=y"
args: "use_celery=y rest_api=DRF"
- name: Gulp
args: "frontend_pipeline=Gulp"
- name: Webpack
Expand Down Expand Up @@ -72,6 +72,12 @@ jobs:
args: "frontend_pipeline=Webpack use_heroku=y"
- name: Email Username
args: "username_type=email ci_tool=Github project_name='Something superduper long - the great amazing project' project_slug=my_awesome_project"
- name: Email username & DRF
args: "username_type=email rest_api=DRF ci_tool=Gitlab"
- name: Async & Django-Ninja
args: "use_async=y rest_api='Django Ninja'"
- name: Async, email username & Django-Ninja
args: "use_async=y username_type=email rest_api='Django Ninja'"

name: "Bare metal ${{ matrix.script.name }}"
runs-on: ubuntu-latest
Expand Down
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,8 +150,12 @@ Answer the prompts with your own desired [options](http://cookiecutter-django.re
8 - SparkPost
9 - Other SMTP
Choose from 1, 2, 3, 4, 5, 6, 7, 8, 9 [1]: 1
Select rest_api [n]:
1 - None
2 - DRF
3 - Django Ninja
Choose from 1, 2, 3 [1]: 1
use_async [n]: n
use_drf [n]: y
Select frontend_pipeline:
1 - None
2 - Django Compressor
Expand Down
2 changes: 1 addition & 1 deletion cookiecutter.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@
"SparkPost",
"Other SMTP"
],
"rest_api": ["None", "DRF", "Django Ninja"],
"use_async": "n",
"use_drf": "n",
"frontend_pipeline": ["None", "Django Compressor", "Gulp", "Webpack"],
"use_celery": "n",
"use_mailpit": "n",
Expand Down
11 changes: 8 additions & 3 deletions docs/1-getting-started/project-generation-options.rst
Original file line number Diff line number Diff line change
Expand Up @@ -94,12 +94,16 @@ mail_service:
8. SparkPost_
9. `Other SMTP`_

rest_api:
Select a REST API framework to use. The choices are:

1. None
2. `Django Rest Framework`_
3. `Django Ninja`_

use_async:
Indicates whether the project should use web sockets with Uvicorn + Gunicorn.

use_drf:
Indicates whether the project should be configured to use `Django Rest Framework`_.

frontend_pipeline:
Select a pipeline to compile and optimise frontend assets (JS, CSS, ...):

Expand Down Expand Up @@ -178,6 +182,7 @@ debug:
.. _Other SMTP: https://anymail.readthedocs.io/en/stable/

.. _Django Rest Framework: https://github.com/encode/django-rest-framework/
.. _Django Ninja: https://github.com/vitalik/django-ninja

.. _Django Compressor: https://github.com/django-compressor/django-compressor

Expand Down
17 changes: 16 additions & 1 deletion hooks/post_gen_project.py
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,17 @@ def remove_aws_dockerfile():

def remove_drf_starter_files():
Path("config", "api_router.py").unlink()
Path("{{cookiecutter.project_slug}}", "users", "api", "serializers.py").unlink()


def remove_ninja_starter_files():
Path("config", "api.py").unlink()
Path("{{cookiecutter.project_slug}}", "users", "api", "schema.py").unlink()


def remove_rest_api_files():
remove_drf_starter_files()
remove_ninja_starter_files()
shutil.rmtree(Path("{{cookiecutter.project_slug}}", "users", "api"))
shutil.rmtree(Path("{{cookiecutter.project_slug}}", "users", "tests", "api"))

Expand Down Expand Up @@ -499,8 +510,12 @@ def main(): # noqa: C901, PLR0912, PLR0915
if "{{ cookiecutter.ci_tool }}" != "Drone":
remove_dotdrone_file()

if "{{ cookiecutter.use_drf }}".lower() == "n":
if "{{ cookiecutter.rest_api }}" == "DRF":
remove_ninja_starter_files()
elif "{{ cookiecutter.rest_api }}" == "Django Ninja":
remove_drf_starter_files()
else:
remove_rest_api_files()

if "{{ cookiecutter.use_async }}".lower() == "n":
remove_async_files()
Expand Down
5 changes: 3 additions & 2 deletions tests/test_cookiecutter_generation.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,10 +106,11 @@ def context():
{"cloud_provider": "Azure", "mail_service": "Other SMTP"},
# Note: cloud_providers GCP, Azure, and None
# with mail_service Amazon SES is not supported
{"rest_api": "None"},
{"rest_api": "DRF"},
{"rest_api": "Django Ninja"},
{"use_async": "y"},
{"use_async": "n"},
{"use_drf": "y"},
{"use_drf": "n"},
{"frontend_pipeline": "None"},
{"frontend_pipeline": "Django Compressor"},
{"frontend_pipeline": "Gulp"},
Expand Down
11 changes: 11 additions & 0 deletions {{cookiecutter.project_slug}}/config/api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from django.contrib.admin.views.decorators import staff_member_required
from ninja import NinjaAPI
from ninja.security import SessionAuth

api = NinjaAPI(
urls_namespace="api",
auth=SessionAuth(),
docs_decorator=staff_member_required,
)

api.add_router("/users/", "{{ cookiecutter.project_slug }}.users.api.views.router")
11 changes: 6 additions & 5 deletions {{cookiecutter.project_slug}}/config/settings/base.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
# ruff: noqa: ERA001, E501
"""Base settings to build other settings files upon."""

{% if cookiecutter.use_celery == 'y' -%}
{% if cookiecutter.use_celery == 'y' %}
import ssl
{%- endif %}
from pathlib import Path
Expand Down Expand Up @@ -92,11 +91,13 @@
{%- if cookiecutter.use_celery == 'y' %}
"django_celery_beat",
{%- endif %}
{%- if cookiecutter.use_drf == "y" %}
{%- if cookiecutter.rest_api == 'DRF' %}
"rest_framework",
"rest_framework.authtoken",
"corsheaders",
"drf_spectacular",
{%- elif cookiecutter.rest_api == 'Django Ninja' %}
"corsheaders",
{%- endif %}
{%- if cookiecutter.frontend_pipeline == 'Webpack' %}
"webpack_loader",
Expand Down Expand Up @@ -154,7 +155,7 @@
# https://docs.djangoproject.com/en/dev/ref/settings/#middleware
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
{%- if cookiecutter.use_drf == 'y' %}
{%- if cookiecutter.rest_api != 'None' %}
"corsheaders.middleware.CorsMiddleware",
{%- endif %}
{%- if cookiecutter.use_whitenoise == 'y' %}
Expand Down Expand Up @@ -361,7 +362,7 @@
INSTALLED_APPS += ["compressor"]
STATICFILES_FINDERS += ["compressor.finders.CompressorFinder"]
{%- endif %}
{% if cookiecutter.use_drf == "y" -%}
{% if cookiecutter.rest_api == 'DRF' -%}
# django-rest-framework
# -------------------------------------------------------------------------------
# django-rest-framework - https://www.django-rest-framework.org/api-guide/settings/
Expand Down
3 changes: 2 additions & 1 deletion {{cookiecutter.project_slug}}/config/settings/local.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@
{%- else -%}
# https://docs.djangoproject.com/en/dev/ref/settings/#email-backend
EMAIL_BACKEND = env(
"DJANGO_EMAIL_BACKEND", default="django.core.mail.backends.console.EmailBackend",
"DJANGO_EMAIL_BACKEND",
default="django.core.mail.backends.console.EmailBackend",
)
{%- endif %}

Expand Down
4 changes: 2 additions & 2 deletions {{cookiecutter.project_slug}}/config/settings/production.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from .base import DATABASES
from .base import INSTALLED_APPS
from .base import REDIS_URL
{%- if cookiecutter.use_drf == "y" %}
{%- if cookiecutter.rest_api == 'DRF' %}
from .base import SPECTACULAR_SETTINGS
{%- endif %}
from .base import env
Expand Down Expand Up @@ -436,7 +436,7 @@
traces_sample_rate=env.float("SENTRY_TRACES_SAMPLE_RATE", default=0.0),
)
{% endif %}
{% if cookiecutter.use_drf == "y" -%}
{% if cookiecutter.rest_api == 'DRF' -%}

# django-rest-framework
# -------------------------------------------------------------------------------
Expand Down
14 changes: 12 additions & 2 deletions {{cookiecutter.project_slug}}/config/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,13 @@
from django.urls import path
from django.views import defaults as default_views
from django.views.generic import TemplateView
{%- if cookiecutter.use_drf == 'y' %}
{%- if cookiecutter.rest_api == 'DRF' %}
from drf_spectacular.views import SpectacularAPIView
from drf_spectacular.views import SpectacularSwaggerView
from rest_framework.authtoken.views import obtain_auth_token
{%- elif cookiecutter.rest_api == 'Django Ninja' %}

from .api import api
{%- endif %}

urlpatterns = [
Expand All @@ -36,7 +39,7 @@
# Static file serving when using Gunicorn + Uvicorn for local web socket development
urlpatterns += staticfiles_urlpatterns()
{%- endif %}
{% if cookiecutter.use_drf == 'y' %}
{% if cookiecutter.rest_api == 'DRF' %}
# API URLS
urlpatterns += [
# API base url
Expand All @@ -50,6 +53,13 @@
name="api-docs",
),
]
{%- elif cookiecutter.rest_api == 'Django Ninja' %}

# API URLS
urlpatterns += [
# API base url
path("api/", api.urls),
]
{%- endif %}

if settings.DEBUG:
Expand Down
1 change: 1 addition & 0 deletions {{cookiecutter.project_slug}}/manage.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""

import os
import sys
from pathlib import Path
Expand Down
2 changes: 1 addition & 1 deletion {{cookiecutter.project_slug}}/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ warn_redundant_casts = true
warn_unused_configs = true
plugins = [
"mypy_django_plugin.main",
{%- if cookiecutter.use_drf == "y" %}
{%- if cookiecutter.rest_api == 'DRF' %}
"mypy_drf_plugin.main",
{%- endif %}
]
Expand Down
6 changes: 5 additions & 1 deletion {{cookiecutter.project_slug}}/requirements/base.txt
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,16 @@ crispy-bootstrap5==2025.6 # https://github.com/django-crispy-forms/crispy-boots
django-compressor==4.5.1 # https://github.com/django-compressor/django-compressor
{%- endif %}
django-redis==6.0.0 # https://github.com/jazzband/django-redis
{%- if cookiecutter.use_drf == 'y' %}
{%- if cookiecutter.rest_api == 'DRF' %}
# Django REST Framework
djangorestframework==3.16.1 # https://github.com/encode/django-rest-framework
django-cors-headers==4.9.0 # https://github.com/adamchainz/django-cors-headers
# DRF-spectacular for api documentation
drf-spectacular==0.28.0 # https://github.com/tfranzel/drf-spectacular
{%- elif cookiecutter.rest_api == 'Django Ninja' %}
# Django Ninja
django-ninja==1.4.3 # https://github.com/vitalik/django-ninja
django-cors-headers==4.9.0 # https://github.com/adamchainz/django-cors-headers
{%- endif %}
{%- if cookiecutter.frontend_pipeline == 'Webpack' %}
django-webpack-loader==3.2.1 # https://github.com/django-webpack/django-webpack-loader
Expand Down
2 changes: 1 addition & 1 deletion {{cookiecutter.project_slug}}/requirements/local.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ mypy==1.18.2 # https://github.com/python/mypy
django-stubs[compatible-mypy]==5.2.5 # https://github.com/typeddjango/django-stubs
pytest==8.4.2 # https://github.com/pytest-dev/pytest
pytest-sugar==1.1.1 # https://github.com/Teemu/pytest-sugar
{%- if cookiecutter.use_drf == "y" %}
{%- if cookiecutter.rest_api == 'DRF' %}
djangorestframework-stubs==3.16.4 # https://github.com/typeddjango/djangorestframework-stubs
{%- endif %}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from django.urls.base import reverse
from ninja import ModelSchema

from {{ cookiecutter.project_slug }}.users.models import User


class UpdateUserSchema(ModelSchema):
class Meta:
model = User
{%- if cookiecutter.username_type == "email" %}
fields = ["name"]
{%- else %}
fields = ["username", "name"]
{%- endif %}


class UserSchema(ModelSchema):
url: str

class Meta:
model = User
{%- if cookiecutter.username_type == "email" %}
fields = ["email", "name"]
{%- else %}
fields = ["username", "email", "name"]
{%- endif %}

@staticmethod
def resolve_url(obj: User):
{%- if cookiecutter.username_type == "email" %}
return reverse("api:retrieve_user", kwargs={"pk": obj.pk})
{%- else %}
return reverse("api:retrieve_user", kwargs={"username": obj.username})
{%- endif %}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
{% if cookiecutter.rest_api == 'DRF' -%}
from rest_framework import status
from rest_framework.decorators import action
from rest_framework.mixins import ListModelMixin
Expand Down Expand Up @@ -28,3 +29,63 @@ def get_queryset(self, *args, **kwargs):
def me(self, request):
serializer = UserSerializer(request.user, context={"request": request})
return Response(status=status.HTTP_200_OK, data=serializer.data)
{%- elif cookiecutter.rest_api == 'Django Ninja' -%}
from django.db.models import QuerySet
from django.shortcuts import get_object_or_404
from ninja import Router

from {{ cookiecutter.project_slug }}.users.api.schema import UpdateUserSchema
from {{ cookiecutter.project_slug }}.users.api.schema import UserSchema
from {{ cookiecutter.project_slug }}.users.models import User

router = Router(tags=["users"])


def _get_users_queryset(request) -> QuerySet[User]:
return User.objects.filter(pk=request.user.pk)


@router.get("/", response=list[UserSchema])
def list_users(request):
return _get_users_queryset(request)
{%- if cookiecutter.username_type == "email" %}


@router.get("/{pk}/", response=UserSchema)
def retrieve_user(request, pk: str):
if pk == "me":
return request.user
users_qs = _get_users_queryset(request)
return get_object_or_404(users_qs, pk=pk)
{%- else %}


@router.get("/{username}/", response=UserSchema)
def retrieve_user(request, username: str):
if username == "me":
return request.user
users_qs = _get_users_queryset(request)
return get_object_or_404(users_qs, username=username)
{%- endif %}
{%- if cookiecutter.username_type == "email" %}


@router.patch("/{pk}/", response=UserSchema)
def update_user(request, pk: str, data: UpdateUserSchema):
users_qs = _get_users_queryset(request)
user = get_object_or_404(users_qs, pk=pk)
user.name = data.name
user.save()
return user
{%- else %}


@router.patch("/{username}/", response=UserSchema)
def update_user(request, username: str, data: UpdateUserSchema):
users_qs = _get_users_queryset(request)
user = get_object_or_404(users_qs, username=username)
user.name = data.name
user.save()
return user
{%- endif %}
{%- endif %}
Loading