Skip to content

Commit e6dbb90

Browse files
nvo87nkiryanov
andauthored
Bumped python and django. Updated .django-app-template. (#564)
* bumped python and django. Updated .django-app-template. * set template file naming according to app_name * add default viewsets * Fix circle ci and Dockerfile update (#565) 1. Circle: update orb version, cause the previous one deprecated 2. Circle: explicitly set python 3.11 version to use for test and build 3. Dockerfile: update debian version to last one 4. Dockerfile: update UWSGI cause previous doesn't support python 3.11 --------- Co-authored-by: Nikolay Kiryanov <nkiryanov@fands.dev>
1 parent accb892 commit e6dbb90

File tree

19 files changed

+210
-22
lines changed

19 files changed

+210
-22
lines changed

.circleci/config.yml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
version: 2.1
22
orbs:
3-
docker: circleci/docker@1.7.0
3+
docker: circleci/docker@2.4.0
44

55
jobs:
66
build:
77
docker:
8-
- image: cimg/python:3.10
8+
- image: cimg/python:3.11
99

1010
steps:
1111
- checkout
@@ -34,7 +34,7 @@ jobs:
3434

3535
coverage:
3636
docker:
37-
- image: cimg/python:3.10
37+
- image: cimg/python:3.11
3838
steps:
3939
- checkout
4040
- attach_workspace:
@@ -73,5 +73,6 @@ workflows:
7373
image: f213/django
7474
path: testproject/django
7575
docker-context: testproject/django
76+
extra_build_args: '--build-arg PYTHON_VERSION=3.11'
7677
deploy: false
7778
attach-at: .
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
3.10.8
1+
3.11.6

{{cookiecutter.project_slug}}/Dockerfile

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
ARG PYTHON_VERSION
2-
FROM python:${PYTHON_VERSION}-slim-bullseye
2+
FROM python:${PYTHON_VERSION}-slim-bookworm
33
LABEL maintainer="fedor@borshev.com"
44

55
LABEL com.datadoghq.ad.logs='[{"source": "uwsgi", "service": "django"}]'
@@ -9,9 +9,9 @@ ENV DEBIAN_FRONTEND noninteractive
99

1010
ENV STATIC_ROOT /static
1111

12-
ENV _UWSGI_VERSION 2.0.20
12+
ENV _UWSGI_VERSION 2.0.23
1313

14-
RUN echo deb http://deb.debian.org/debian bullseye contrib non-free > /etc/apt/sources.list.d/debian-contrib.list \
14+
RUN echo deb http://deb.debian.org/debian bookworm contrib non-free > /etc/apt/sources.list.d/debian-contrib.list \
1515
&& apt update \
1616
&& apt --no-install-recommends install -y gettext locales-all wget \
1717
imagemagick tzdata wait-for-it build-essential \

{{cookiecutter.project_slug}}/Makefile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,5 @@ test:
2727
cd src && ./manage.py compilemessages
2828
cd src && pytest --dead-fixtures
2929
cd src && pytest -x
30+
31+
pr: fmt lint test

{{cookiecutter.project_slug}}/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
name = "{{cookiecutter.project_slug}}"
33
version = "{{cookiecutter.project_version}}"
44
dependencies = [
5-
"Django<3.3",
5+
"Django<4.3",
66
"bcrypt",
77
"django-behaviors",
88
"django-environ",

{{cookiecutter.project_slug}}/src/.django-app-template/api/serializers.py-tpl

Lines changed: 0 additions & 3 deletions
This file was deleted.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
__all__ = [
2+
"",
3+
]
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from rest_framework import serializers
2+
3+
# Rename this file to singular form of your entity, e.g. "orders.py -> order.py". Add your class to __init__.py.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
__all__ = [
2+
"",
3+
]
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from app.api.viewsets import DefaultModelViewSet
2+
3+
# Rename this file to singular form of your entity, e.g. "orders.py -> order.py". Add your class to __init__.py.

{{cookiecutter.project_slug}}/src/.django-app-template/api/viewsets.py-tpl

Lines changed: 0 additions & 4 deletions
This file was deleted.

{{cookiecutter.project_slug}}/src/.django-app-template/models.py-tpl

Lines changed: 0 additions & 5 deletions
This file was deleted.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
__all__ = [
2+
"",
3+
]
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from django.db import models
2+
3+
from app.models import DefaultModel, TimestampedModel
4+
5+
# Rename this file to singular form of your entity, e.g. "orders.py -> order.py". Add your class to __init__.py.
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
from typing import Any, Optional, Protocol, Type
2+
3+
from rest_framework import mixins
4+
from rest_framework import status
5+
from rest_framework.mixins import CreateModelMixin
6+
from rest_framework.mixins import UpdateModelMixin
7+
from rest_framework.request import Request
8+
from rest_framework.response import Response
9+
from rest_framework.serializers import BaseSerializer
10+
from rest_framework.viewsets import GenericViewSet
11+
12+
__all__ = ["DefaultModelViewSet"]
13+
14+
15+
class BaseGenericViewSet(Protocol):
16+
def get_serializer(self, *args: Any, **kwargs: Any) -> Any:
17+
...
18+
19+
def get_response(self, *args: Any, **kwargs: Any) -> Any:
20+
...
21+
22+
def perform_create(self, *args: Any, **kwargs: Any) -> Any:
23+
...
24+
25+
def perform_update(self, *args: Any, **kwargs: Any) -> Any:
26+
...
27+
28+
def get_success_headers(self, *args: Any, **kwargs: Any) -> Any:
29+
...
30+
31+
def get_serializer_class(self, *args: Any, **kwargs: Any) -> Any:
32+
...
33+
34+
def get_object(self, *args: Any, **kwargs: Any) -> Any:
35+
...
36+
37+
38+
class DefaultCreateModelMixin(CreateModelMixin):
39+
"""Return detail-serialized created instance"""
40+
41+
def create(self: BaseGenericViewSet, request: Request, *args: Any, **kwargs: Any) -> Response:
42+
serializer = self.get_serializer(data=request.data)
43+
serializer.is_valid(raise_exception=True)
44+
instance = self.perform_create(serializer) # No getting created instance in original DRF
45+
headers = self.get_success_headers(serializer.data)
46+
return self.get_response(instance, status.HTTP_201_CREATED, headers)
47+
48+
def perform_create(self: BaseGenericViewSet, serializer: Any) -> Any:
49+
return serializer.save() # No returning created instance in original DRF
50+
51+
52+
class DefaultUpdateModelMixin(UpdateModelMixin):
53+
"""Return detail-serialized updated instance"""
54+
55+
def update(self: BaseGenericViewSet, request: Request, *args: Any, **kwargs: Any) -> Response:
56+
partial = kwargs.pop("partial", False)
57+
instance = self.get_object()
58+
serializer = self.get_serializer(instance, data=request.data, partial=partial)
59+
serializer.is_valid(raise_exception=True)
60+
instance = self.perform_update(serializer) # No getting updated instance in original DRF
61+
62+
if getattr(instance, "_prefetched_objects_cache", None):
63+
# If 'prefetch_related' has been applied to a queryset, we need to
64+
# forcibly invalidate the prefetch cache on the instance.
65+
instance._prefetched_objects_cache = {}
66+
67+
return self.get_response(instance, status.HTTP_200_OK)
68+
69+
def perform_update(self: BaseGenericViewSet, serializer: Any) -> Any:
70+
return serializer.save() # No returning updated instance in original DRF
71+
72+
73+
class ResponseWithRetrieveSerializerMixin:
74+
"""
75+
Always response with 'retrieve' serializer or fallback to `serializer_class`.
76+
Usage:
77+
78+
class MyViewSet(DefaultModelViewSet):
79+
serializer_class = MyDefaultSerializer
80+
serializer_action_classes = {
81+
'list': MyListSerializer,
82+
'my_action': MyActionSerializer,
83+
}
84+
@action
85+
def my_action:
86+
...
87+
88+
'my_action' request will be validated with MyActionSerializer,
89+
but response will be serialized with MyDefaultSerializer
90+
(or 'retrieve' if provided).
91+
92+
Thanks gonz: http://stackoverflow.com/a/22922156/11440
93+
94+
"""
95+
96+
def get_response(
97+
self: BaseGenericViewSet,
98+
instance: Any,
99+
status: Any,
100+
headers: Any = None,
101+
) -> Response:
102+
retrieve_serializer_class = self.get_serializer_class(action="retrieve")
103+
context = self.get_serializer_context() # type: ignore
104+
retrieve_serializer = retrieve_serializer_class(instance, context=context)
105+
return Response(
106+
retrieve_serializer.data,
107+
status=status,
108+
headers=headers,
109+
)
110+
111+
def get_serializer_class(
112+
self: BaseGenericViewSet,
113+
action: Optional[str] = None,
114+
) -> Type[BaseSerializer]:
115+
if action is None:
116+
action = self.action # type: ignore
117+
118+
try:
119+
return self.serializer_action_classes[action] # type: ignore
120+
except (KeyError, AttributeError):
121+
return super().get_serializer_class() # type: ignore
122+
123+
124+
class DefaultModelViewSet(
125+
DefaultCreateModelMixin, # Create response is overriden
126+
mixins.RetrieveModelMixin,
127+
DefaultUpdateModelMixin, # Update response is overriden
128+
mixins.DestroyModelMixin,
129+
mixins.ListModelMixin,
130+
ResponseWithRetrieveSerializerMixin, # Response with retrieve or default serializer
131+
GenericViewSet,
132+
):
133+
pass
134+
135+
136+
class ReadonlyModelViewSet(
137+
mixins.RetrieveModelMixin,
138+
mixins.ListModelMixin,
139+
ResponseWithRetrieveSerializerMixin, # Response with retrieve or default serializer
140+
GenericViewSet,
141+
):
142+
pass
143+
144+
145+
class ListOnlyModelViewSet(
146+
mixins.ListModelMixin,
147+
ResponseWithRetrieveSerializerMixin, # Response with retrieve or default serializer
148+
GenericViewSet,
149+
):
150+
pass
151+
152+
153+
class UpdateOnlyModelViewSet(
154+
DefaultUpdateModelMixin,
155+
ResponseWithRetrieveSerializerMixin,
156+
GenericViewSet,
157+
):
158+
pass
159+
160+
161+
class DefaultRetrieveDestroyListViewSet(
162+
mixins.RetrieveModelMixin,
163+
mixins.DestroyModelMixin,
164+
mixins.ListModelMixin,
165+
ResponseWithRetrieveSerializerMixin, # Response with retrieve or default serializer
166+
GenericViewSet,
167+
):
168+
pass
169+
170+
171+
class ListUpdateModelViewSet(
172+
DefaultUpdateModelMixin,
173+
mixins.ListModelMixin,
174+
ResponseWithRetrieveSerializerMixin,
175+
GenericViewSet,
176+
):
177+
pass

{{cookiecutter.project_slug}}/src/app/urls/v1.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66

77
app_name = "api_v1"
88
urlpatterns = [
9-
path("auth/", include("a12n.urls")),
10-
path("users/", include("users.urls")),
9+
path("auth/", include("a12n.api.urls")),
10+
path("users/", include("users.api.urls")),
1111
path("healthchecks/", include("django_healthchecks.urls")),
1212
path("docs/schema/", SpectacularAPIView.as_view(), name="schema"),
1313
path("docs/swagger/", SpectacularSwaggerView.as_view(url_name="schema")),

0 commit comments

Comments
 (0)