Skip to content

Commit

Permalink
Merge branch 'feat/typescript' into td/dependencies-upgrade
Browse files Browse the repository at this point in the history
  • Loading branch information
pamella committed May 13, 2024
2 parents de61ab3 + 37da448 commit 427945b
Show file tree
Hide file tree
Showing 13 changed files with 213 additions and 7 deletions.
6 changes: 6 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,9 @@ repos:
# Only run missing migration check if migration-generating files have changed:
files: (.*/?(settings|migrations|models)/.+|.+models\.py|.+constants\.py|.+choices\.py|.+pyproject\.toml)
pass_filenames: false
- id: backend-schema
name: backend-schema-local
entry: poetry run python backend/manage.py spectacular --color --file backend/schema.yml
language: system
files: ^backend/
pass_filenames: false
7 changes: 7 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ backend_format:
docker_setup:
docker volume create {{project_name}}_dbdata
docker compose build --no-cache backend
docker compose run --rm backend python manage.py spectacular --color --file schema.yml
docker compose run frontend npm install

docker_test:
Expand All @@ -44,3 +45,9 @@ docker_makemigrations:

docker_migrate:
docker compose run --rm backend python manage.py migrate

docker_backend_shell:
docker compose run --rm backend bash

docker_backend_update_schema:
docker compose run --rm backend python manage.py spectacular --color --file schema.yml
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ Also, includes a Render.com `render.yaml` and a working Django `production.py` s

- `django` for building backend logic using Python
- `djangorestframework` for building a REST API on top of Django
- `drf-spectacular` for generating an OpenAPI schema for the Django REST API
- `django-webpack-loader` for rendering the bundled frontend assets
- `django-js-reverse` for easy handling of Django URLs on JS
- `django-upgrade` for automatically upgrading Django code to the target version on pre-commit
Expand Down Expand Up @@ -167,6 +168,8 @@ After completing ALL of the above, remove this `Project bootstrap` section from
`poetry run python manage.py makemigrations`
- Run the migrations:
`poetry run python manage.py migrate`
- Generate the OpenAPI schema:
`poetry run python manage.py spectacular --color --file schema.yml`
- Run the project:
`poetry run python manage.py runserver`
- Open a browser and go to `http://localhost:8000` to see the project running
Expand Down Expand Up @@ -201,6 +204,19 @@ Will run django tests using `--keepdb` and `--parallel`. You may pass a path to
To add a new **backend** dependency, run `poetry add {dependency}`. If the dependency should be only available for development user append `-G dev` to the command.
### API Schema
We use the DRF-Spectacular tool to generate an OpenAPI schema from our Django Rest Framework API. The OpenAPI schema serves as the backbone for generating client code, creating comprehensive API documentation, and more.
The API documentation pages are accessible at `http://localhost:8000/api/schema/swagger-ui/` or `http://localhost:8000/api/schema/redoc/`.
> [!IMPORTANT]
> Anytime a view is created, updated, or removed, the schema must be updated to reflect the changes. Failing to do so can lead to outdated client code or documentation.
>
> To update the schema, run:
> - If you are using Docker: `make docker_backend_update_schema`
> - If you are not using Docker: `poetry run python manage.py spectacular --color --file schema.yml`
## Github Actions
To enable Continuous Integration through Github Actions, we provide a `proj_main.yml` file. To connect it to Github you need to rename it to `main.yml` and move it to the `.github/workflows/` directory.
Expand Down
9 changes: 7 additions & 2 deletions backend/common/utils/tests.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from django.test import Client, TestCase
from django.test import TestCase
from django.urls import reverse

from model_bakery import baker
from rest_framework.test import APIClient


class TestCaseUtils(TestCase):
Expand All @@ -11,7 +12,7 @@ def setUp(self):
self.user.set_password(self._user_password)
self.user.save()

self.auth_client = Client()
self.auth_client = APIClient()
self.auth_client.login(email=self.user.email, password=self._user_password)

def reverse(self, name, *args, **kwargs):
Expand All @@ -26,6 +27,10 @@ def assertResponse201(self, response):
"""Given response has status_code 201 CREATED"""
self.assertEqual(response.status_code, 201)

def assertResponse204(self, response):
"""Given response has status_code 204 NO CONTENT"""
self.assertEqual(response.status_code, 204)

def assertResponse301(self, response):
"""Given response has status_code 301 MOVED PERMANENTLY"""
self.assertEqual(response.status_code, 301)
Expand Down
21 changes: 21 additions & 0 deletions backend/common/views.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from django.views import generic

from drf_spectacular.utils import OpenApiExample, OpenApiResponse, extend_schema
from rest_framework import status, viewsets
from rest_framework.decorators import action
from rest_framework.permissions import AllowAny
Expand All @@ -11,6 +12,26 @@ class IndexView(generic.TemplateView):


class RestViewSet(viewsets.ViewSet):
@extend_schema(
summary="Check REST API",
description="This endpoint checks if the REST API is working.",
responses={
200: OpenApiResponse(
description="Successful Response",
examples=[
OpenApiExample(
"Example Response",
value={
"result": "This message comes from the backend. "
"If you're seeing this, the REST API is working!"
},
response_only=True,
)
],
)
},
methods=["GET"],
)
@action(
detail=False,
methods=["get"],
Expand Down
28 changes: 27 additions & 1 deletion backend/project_name/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ def base_dir_join(*args):
"webpack_loader",
"import_export",
"rest_framework",
"drf_spectacular",
"defender",
"django_guid",
"common",
Expand Down Expand Up @@ -112,6 +113,15 @@ def base_dir_join(*args):
"DEFAULT_PERMISSION_CLASSES": [
"rest_framework.permissions.IsAuthenticated",
],
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
}

# drf-spectacular
SPECTACULAR_SETTINGS = {
"TITLE": "Vinta Boilerplate API",
"DESCRIPTION": "A Django project boilerplate with Vinta's best practices",
"VERSION": "0.1.0",
"SERVE_INCLUDE_SCHEMA": False,
}

LANGUAGE_CODE = "en-us"
Expand Down Expand Up @@ -204,6 +214,10 @@ def base_dir_join(*args):
"'unsafe-inline'",
"'unsafe-eval'",
"https://browser.sentry-cdn.com",
# drf-spectacular UI (Swagger and ReDoc)
"https://cdn.jsdelivr.net/npm/swagger-ui-dist@latest/",
"https://cdn.jsdelivr.net/npm/redoc@latest/",
"blob:",
] + [f"*{host}" if host.startswith(".") else host for host in ALLOWED_HOSTS]
CSP_CONNECT_SRC = [
"'self'",
Expand All @@ -212,12 +226,24 @@ def base_dir_join(*args):
CSP_STYLE_SRC = [
"'self'",
"'unsafe-inline'",
# drf-spectacular UI (Swagger and ReDoc)
"https://cdn.jsdelivr.net/npm/swagger-ui-dist@latest/",
"https://cdn.jsdelivr.net/npm/redoc@latest/",
"https://fonts.googleapis.com",
]
CSP_FONT_SRC = [
"'self'",
"'unsafe-inline'",
# drf-spectacular UI (Swagger and ReDoc)
"https://fonts.gstatic.com",
] + [f"*{host}" if host.startswith(".") else host for host in ALLOWED_HOSTS]
CSP_IMG_SRC = ["'self'"]
CSP_IMG_SRC = [
"'self'",
# drf-spectacular UI (Swagger and ReDoc)
"data:",
"https://cdn.jsdelivr.net/npm/swagger-ui-dist@latest/",
"https://cdn.redoc.ly/redoc/",
]

# Django-defender
DEFENDER_LOGIN_FAILURE_LIMIT = 3
Expand Down
20 changes: 19 additions & 1 deletion backend/project_name/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,18 @@

import django_js_reverse.views
from common.routes import routes as common_routes
from drf_spectacular.views import (
SpectacularAPIView,
SpectacularRedocView,
SpectacularSwaggerView,
)
from rest_framework.routers import DefaultRouter
from users.routes import routes as users_routes


router = DefaultRouter()

routes = common_routes
routes = common_routes + users_routes
for route in routes:
router.register(route["regex"], route["viewset"], basename=route["basename"])

Expand All @@ -18,4 +24,16 @@
path("admin/defender/", include("defender.urls")),
path("jsreverse/", django_js_reverse.views.urls_js, name="js_reverse"),
path("api/", include(router.urls), name="api"),
# drf-spectacular
path("api/schema/", SpectacularAPIView.as_view(), name="schema"),
path(
"api/schema/swagger-ui/",
SpectacularSwaggerView.as_view(url_name="schema"),
name="swagger-ui",
),
path(
"api/schema/redoc/",
SpectacularRedocView.as_view(url_name="schema"),
name="redoc",
),
]
6 changes: 6 additions & 0 deletions backend/users/routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from .views import UserViewSet


routes = [
{"regex": r"users", "viewset": UserViewSet, "basename": "user"},
]
18 changes: 18 additions & 0 deletions backend/users/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from rest_framework import serializers

from .models import User


class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = [ # noqa: RUF012
"id",
"email",
"is_active",
"is_staff",
"is_superuser",
"created",
"modified",
"last_login",
]
77 changes: 77 additions & 0 deletions backend/users/tests/test_views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
from django.urls import reverse

from common.utils.tests import TestCaseUtils
from model_bakery import baker
from rest_framework.test import APITestCase

from ..models import User


class UserViewSetTest(TestCaseUtils, APITestCase):
def test_list_users(self):
baker.make(User, _fill_optional=True, _quantity=5)

response = self.auth_client.get(reverse("user-list"))

self.assertResponse200(response)
# Note: One user is already created in the setUp method of TestCaseUtils
self.assertEqual(response.data.get("count"), 6)
self.assertEqual(len(response.data.get("results")), 6)

def test_create_user(self):
data = {
"email": "[email protected]",
"password": "12345678",
}

response = self.auth_client.post(reverse("user-list"), data=data)

self.assertResponse201(response)
user = User.objects.get(id=response.data["id"])
self.assertEqual(user.email, data["email"])

def test_retrieve_user(self):
user = baker.make(User, _fill_optional=True)

response = self.auth_client.get(reverse("user-detail", args=[user.id]))

self.assertResponse200(response)
self.assertEqual(response.data["id"], user.id)
self.assertEqual(response.data["email"], user.email)

def test_put_update_user(self):
user = baker.make(User, email="[email protected]", _fill_optional=True)
data = {
"email": "[email protected]",
"password": "87654321",
}

response = self.auth_client.put(
reverse("user-detail", args=[user.id]), data=data
)

self.assertResponse200(response)
user.refresh_from_db()
self.assertEqual(user.email, data["email"])

def test_patch_update_user(self):
user = baker.make(User, email="[email protected]", _fill_optional=True)
data = {
"email": "[email protected]",
}

response = self.auth_client.patch(
reverse("user-detail", args=[user.id]), data=data
)

self.assertResponse200(response)
user.refresh_from_db()
self.assertEqual(user.email, data["email"])

def test_delete_user(self):
user = baker.make(User, _fill_optional=True)

response = self.auth_client.delete(reverse("user-detail", args=[user.id]))

self.assertResponse204(response)
self.assertFalse(User.objects.filter(id=user.id).exists())
9 changes: 7 additions & 2 deletions backend/users/views.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
from django.shortcuts import render # noqa
from rest_framework import viewsets

from .models import User
from .serializers import UserSerializer

# Create your views here.

class UserViewSet(viewsets.ModelViewSet):
queryset = User.objects.all()
serializer_class = UserSerializer
2 changes: 1 addition & 1 deletion proj_main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ jobs:
REDIS_URL: "redis://"
- run: poetry run pre-commit run --all-files
env:
SKIP: ruff,eslint,missing-migrations
SKIP: ruff,eslint,missing-migrations,backend-schema
- run: poetry run python manage.py makemigrations --check --dry-run
env:
DJANGO_SETTINGS_MODULE: "{{project_name}}.settings.production"
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ django-permissions-policy = "^4.18.0"
django-csp = "^3.7"
django-defender = "^0.9.7"
django-guid = "^3.4.0"
drf-spectacular = "^0.27.2"

[tool.poetry.group.dev.dependencies]
coverage = "^7.2.7"
Expand Down

0 comments on commit 427945b

Please sign in to comment.