Skip to content

Commit 2d79212

Browse files
committed
feat: add Django REST Framework support with custom exception handling and CORS configuration
1 parent db6a9b2 commit 2d79212

8 files changed

Lines changed: 118 additions & 2 deletions

File tree

django/cookiecutter.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,6 @@
1313
"username_type": [
1414
"email",
1515
"username"
16-
]
16+
],
17+
"use_drf": "y"
1718
}

django/hooks/post_gen_project.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import sys
2+
from pathlib import Path
3+
4+
project_slug = "{{ cookiecutter.project_slug }}"
5+
project_path = Path(project_slug)
6+
use_drf = "{{ cookiecutter.use_drf }}" == "y"
7+
8+
def _remove_file(file_path: Path) -> None:
9+
"""Remove a file if it exists."""
10+
file_path.unlink(missing_ok=True)
11+
12+
13+
def handle_drf():
14+
if use_drf:
15+
return
16+
17+
_remove_file(project_path / "drf_exception_handler.py")
18+
19+
def main():
20+
handle_drf()
21+
22+
23+
if __name__ == "__main__":
24+
sys.exit(main())

django/{{ cookiecutter.project_slug }}/pyproject.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ dependencies = [
88
"django>=5.2.3",
99
"django-admin-interface>=0.30.0",
1010
"django-debug-toolbar>=5.2.0",
11+
{% if cookiecutter.use_drf == "y" %}
12+
"djangorestframework>=3.16.0",
13+
"django-cors-headers>=4.7.0",
14+
"drf-spectacular>=0.28.0",
15+
{% endif %}
1116
"django-environ>=0.12.0",
1217
"psycopg[binary]>=3.2.9",
1318
"whitenoise>=6.9.0",
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# Third Party Packages
2+
from django.core.exceptions import PermissionDenied
3+
from django.core.exceptions import ValidationError as DjangoValidationError
4+
from django.http import Http404
5+
from rest_framework import exceptions
6+
from rest_framework.renderers import JSONRenderer
7+
from rest_framework.serializers import as_serializer_error
8+
from rest_framework.views import exception_handler
9+
10+
11+
def handle(exc, ctx):
12+
"""
13+
This is a custom exception handler for our APIs.
14+
cf. https://www.django-rest-framework.org/api-guide/exceptions/#custom-exception-handling
15+
"""
16+
if isinstance(exc, DjangoValidationError):
17+
exc = exceptions.ValidationError(as_serializer_error(exc))
18+
19+
if isinstance(exc, Http404):
20+
exc = exceptions.NotFound()
21+
22+
if isinstance(exc, PermissionDenied):
23+
exc = exceptions.PermissionDenied()
24+
25+
if ctx["request"].accepted_renderer.format != "json":
26+
ctx["request"].accepted_renderer = JSONRenderer()
27+
response = exception_handler(exc, ctx)
28+
29+
# If unexpected error occurs (server error, etc.)
30+
if response is None:
31+
return response
32+
33+
if isinstance(exc.detail, (list, dict)):
34+
response.data = {"detail": response.data}
35+
36+
return response

django/{{ cookiecutter.project_slug }}/{{ cookiecutter.project_slug }}/settings/base.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,3 +158,31 @@
158158
"console": {"class": "logging.StreamHandler", "formatter": "verbose"},
159159
},
160160
}
161+
162+
{% if cookiecutter.use_drf %}
163+
REST_FRAMEWORK = {
164+
"DEFAULT_AUTHENTICATION_CLASSES": (
165+
"rest_framework.authentication.SessionAuthentication",
166+
"rest_framework.authentication.BasicAuthentication",
167+
),
168+
"DEFAULT_PERMISSION_CLASSES": (
169+
"rest_framework.permissions.IsAuthenticated",
170+
),
171+
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
172+
"TEST_REQUEST_DEFAULT_FORMAT": "json",
173+
"EXCEPTION_HANDLER": "{{ cookiecutter.project_slug }}.drf_exception_handler.handle",
174+
}
175+
176+
SPECTACULAR_SETTINGS = {
177+
"TITLE": "{{ cookiecutter.project_name }} API",
178+
"DESCRIPTION": "API documentation for {{ cookiecutter.project_name }}",
179+
"VERSION": "0.1",
180+
"SERVE_INCLUDE_SCHEMA": False,
181+
"COMPONENT_SPLIT_REQUEST": True,
182+
"SERVE_PERMISSIONS": [
183+
'rest_framework.permissions.IsAuthenticated',
184+
]
185+
}
186+
187+
188+
{% endif %}

django/{{ cookiecutter.project_slug }}/{{ cookiecutter.project_slug }}/settings/local.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@
1818
FRONTEND_URL,
1919
]
2020

21+
{% if cookiecutter.use_drf == "y" %}
22+
# CORS
23+
CORS_ALLOW_ALL_ORIGINS = True
24+
CORS_ALLOW_CREDENTIALS = True
25+
{% endif %}
2126
# Django Debug Toolbar
2227
INSTALLED_APPS += ["debug_toolbar"] # noqa: F405
2328
MIDDLEWARE += ["debug_toolbar.middleware.DebugToolbarMiddleware"] # noqa: F405

django/{{ cookiecutter.project_slug }}/{{ cookiecutter.project_slug }}/settings/staging.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,11 @@
88
ALLOWED_HOSTS = [".v2.singular-it-test.de"]
99

1010
CSRF_TRUSTED_ORIGINS = ["https://*.v2.singular-it-test.de"]
11+
12+
{% if cookiecutter.use_drf == "y" %}
13+
# CORS
14+
CORS_ALLOW_CREDENTIALS = True
15+
CORS_ALLOWED_ORIGIN_REGEXES = [
16+
r"^https://.*\.v2\.singular-it-test\.de$",
17+
]
18+
{% endif %}

django/{{ cookiecutter.project_slug }}/{{ cookiecutter.project_slug }}/urls.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,18 @@
1919
from django.contrib import admin
2020
from django.urls import path
2121
from django.urls.conf import include
22-
22+
{% if cookiecutter.use_drf == "y" %}
23+
from drf_spectacular.views import (
24+
SpectacularAPIView,
25+
SpectacularSwaggerView,
26+
)
27+
{% endif %}
2328
urlpatterns = [
2429
path(".admin/", admin.site.urls),
30+
{% if cookiecutter.use_drf == "y" %}
31+
path("api/", SpectacularSwaggerView.as_view(), name="swagger-ui"),
32+
path("api/schema/", SpectacularAPIView.as_view(), name="schema"),
33+
{% endif %}
2534
]
2635

2736
if settings.DEBUG and "debug_toolbar" in settings.INSTALLED_APPS:

0 commit comments

Comments
 (0)