👀 Need help with your Django project? HackSoft is here for you. Reach out at
consulting@hacksoft.io
Table of contents:
docker compose
docker compose
Few points to navigate yourself:
That's about it ✨
Hello 👋
This projects serves as the following:
If you want to learn more about the Django Styleguide, you can watch the videos below:
Radoslav Georgiev's Django structure for scale and longevity for the philosophy behind the styleguide:
Radoslav Georgiev & Ivaylo Bachvarov's discussion on HackCast, around the Django Styleguide:
The initial structure was inspired by cookiecutter-django.
The structure now is modified based on our work & production experience with Django.
Few important things:
docker compose
.whitenoise
setup, even for local development.mypy
configured, using both https://github.com/typeddjango/django-stubs and https://github.com/typeddjango/djangorestframework-stubs/
mypy
configuration is located in setup.cfg
mypy
is ran as a build step in .github/workflows/django.yml
django-filter
for filtering & pagination from DRF.The project is running django-cors-headers
with the following general configuration:
CORS_ALLOW_CREDENTIALS = True
CORS_ALLOW_ALL_ORIGINS = True
For production.py
, we have the following:
CORS_ALLOW_ALL_ORIGINS = False
CORS_ORIGIN_WHITELIST = env.list('CORS_ORIGIN_WHITELIST', default=[])
The project is using https://github.com/Styria-Digital/django-rest-framework-jwt for having authentication via JWT capabilities.
All JWT related settings are located in config/settings/jwt.py
.
⚠️ We highly recommend reading the entire settings page from the project documentation - https://styria-digital.github.io/django-rest-framework-jwt/#additional-settings - to figure out your needs & the proper defaults for you!
The default settings also include the JWT token as a cookie.
The specific details about how the cookie is set, can be found here - https://github.com/Styria-Digital/django-rest-framework-jwt/blob/master/src/rest_framework_jwt/compat.py#L43
The JWT related APIs are:
/api/auth/jwt/login/
/api/auth/jwt/logout/
The current implementation of the login API returns just the token:
{
"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6InJhZG9yYWRvQGhhY2tzb2Z0LmlvIiwiaWF0IjoxNjQxMjIxMDMxLCJleHAiOjE2NDE4MjU4MzEsImp0aSI6ImIyNTEyNmY4LTM3ZDctNGI5NS04Y2M0LTkzZjI3MjE4ZGZkOSIsInVzZXJfaWQiOjJ9.TUoQQPSijO2O_3LN-Pny4wpQp-0rl4lpTs_ulkbxzO4"
}
This can be changed from auth_jwt_response_payload_handler
.
We follow this concept:
ApiAuthMixin
to it.This project is using the already existing cookie-based session authentication in Django:
sessionid
cookie:sessionid=5yic8rov868prmfoin2vhtg4vx35h71p; expires=Tue, 13 Apr 2021 11:17:58 GMT; HttpOnly; Max-Age=1209600; Path=/; SameSite=Lax
axios
:axios.get(url, { withCredentials: true });
axios.post(url, data, { withCredentials: true });
For convenience, CSRF_USE_SESSIONS
is set to True
Check config/settings/sessions.py
for all configuration that's related to sessions.
SessionAuthentication
Since the default implementation of SessionAuthentication
enforces CSRF check, which is not the desired behavior for our APIs, we've done the following:
from rest_framework.authentication import SessionAuthentication
class CsrfExemptedSessionAuthentication(SessionAuthentication):
"""
DRF SessionAuthentication is enforcing CSRF, which may be problematic.
That's why we want to make sure we are exempting any kind of CSRF checks for APIs.
"""
def enforce_csrf(self, request):
return
Which is then used to construct an ApiAuthMixin
, which marks an API that requires authentication:
from rest_framework.permissions import IsAuthenticated
class ApiAuthMixin:
authentication_classes = (CsrfExemptedSessionAuthentication, )
permission_classes = (IsAuthenticated, )
By default, all APIs are public, unless you add the ApiAuthMixin
We have the following general cases:
localhost
development.*.domain.com
and the frontend is located on *.domain.com
, the configuration is going to work out of the box.somedomain.com
and the frontend is located on anotherdomain.com
, then you'll need to set SESSION_COOKIE_SAMESITE = 'None'
and SESSION_COOKIE_SECURE = True
POST
to /api/auth/session/login/
requires JSON body with email
and password
.GET
to /api/auth/me/
returns the current user information, if the request is authenticated (has the corresponding sessionid
cookie)GET
or POST
to /api/auth/logout/
will remove the sessionid
cookie, effectively logging you out.HTTP Only
/ SameSite
The current implementation of /api/auth/session/login
does 2 things:
HTTP Only
cookie with the session id.The second thing is required, because Safari is not respecting the SameSite = None
option for cookies.
More on the issue here - https://www.chromium.org/updates/same-site/incompatible-clients
Since cookies can be somewhat elusive, check the following urls:
SESSION_*
You can find the UserListApi
in styleguide_example/users/apis.py
List API is located at:
http://localhost:8000/api/users/
The API can be filtered:
Example data structure:
{
"limit": 1,
"offset": 0,
"count": 4,
"next": "http://localhost:8000/api/users/?limit=1&offset=1",
"previous": null,
"results": [
{
"id": 1,
"email": "radorado@hacksoft.io",
"is_admin": false
}
]
}
Following this article - https://www.hacksoft.io/blog/direct-to-s3-file-upload-with-django - there's a rich file-upload implementation in the Django Styleguide Example.
Everything is located in the files
app.
Configuration wise, everything is located in config/settings/files_and_storages.py
Additionally, you can check the available options in .env.example
Currently, the following is supported:
Feel free to use this as the basis of your file upload needs.
docker compose
To create Postgres database:
sudo -u postgres createdb -O your_postgres_user_here database_name_here
If you want to recreate your database, you can use the bootstrap script:
./scripts/bootstrap.sh your_postgres_user_here
To start Celery:
celery -A styleguide_example.tasks worker -l info --without-gossip --without-mingle --without-heartbeat
To start Celery Beat:
celery -A styleguide_example.tasks beat -l info --scheduler django_celery_beat.schedulers:DatabaseScheduler
docker compose
To build and run everything
docker compose up
To run migrations
docker compose run django python manage.py migrate
To shell
docker compose run django python manage.py shell
This project is ready to be deployed either on Heroku Render or AWS ECS.
Deploying a Python / Django application on Heroku is quite straighforward & this project is ready to be deployed.
To get an overview of how Heroku deployment works, we recommend reading this first - https://devcenter.heroku.com/articles/deploying-python
Files related to Heroku deployment:
Procfile
web
, worker
and beat
processes.release
phase to run migrations safely, before releasing the new build.runtime.txt
requirements.txt
requirements.txt
, so we've added that.Additionally, you need to specify at least the following settings:
DJANGO_SETTINGS_MODULE
, usually to config.django.production
SECRET_KEY
to something secret. Check here for ideas.ALLOWED_HOSTS
, usually to the default heroku domain (for example - hacksoft-styleguide-example.herokuapp.com
)On top of that, we've added gunicorn.conf.py
with some example settings.
We recommend the following materials, to figure out gunicorn
defaults and configuration:
To get an overview of how Render deployment works, we recommend reading this first - https://render.com/docs/deploy-django
There's a current deployment that can be found here - https://django-styleguide.hacksoft.io/
Files related to Heroku deployment:
render.yaml
docker/*_entrypoint.sh
docker/production.Dockerfile
requirements.txt
requirements.txt
, so we've added that.Coming soon
In all our Django projects we use:
To make sure all of the above tools work in symbiosis, you'd need to add some configuration:
.pre-commit-config.yaml
file to the root of your project. There you can add the instructions for pre-commit
pyproject.toml
file to the root of your project. There you can add the ruff
config.If you are running it as a separate step in the build process:
build:
runs-on: ubuntu-latest
steps:
- name: Run ruff
uses: chartboost/ruff-action@v1
If you would like to run it as a part of another step, which has already ran the package installation commands:
name: Run ruff run: ruff check .
ruff
every time you save a new Python file.In order to test if your local setup is up to date, you can either:
pre-commit
is going to be triggered.ruff check .
in the project root directory.