This template provides a setup to easily develop RESTFUL servers. It implements following features:
- User Management Backend (http://localhost:8000/admin)
- Create Users / Groups
- Define permissions
- Block authorization (Tokens)
- https://docs.djangoproject.com/en/5.0/ref/contrib/admin/
- JWT Authorization
- Is used to authorize REST clients with user credentials
- https://django-rest-framework-simplejwt.readthedocs.io/en/latest/
- Role-based access restrictions
- restricting access based on roles
- Model level permission
- https://www.django-rest-framework.org/api-guide/permissions/
- Access-log
- Needed for sensitive data
- Every request will be logged with username and IP
- Object–relational mapping (ORM)
- Helpful tool to use database table in objects
- Mapping Classes to Data Table
- https://docs.djangoproject.com/en/5.0/topics/db/models/
- Database migration
- Easy set-up of databases
- https://docs.djangoproject.com/en/5.0/topics/migrations/
- Apps
- Django method to split functionalities
- https://docs.djangoproject.com/en/5.0/ref/applications/
- Django REST framework
- Clean structure of functionalities
- Serialization of model objects
- Hyperlink based Model object relations
- Class based view handlers
- https://www.django-rest-framework.org/
- Browsable Swagger API
- Simple example of storing Patient Appointment data
- can be found in ./server/django_web_template/appointment
- Simple example for a client
- found in client/login_test.py
- needs 'requests' library
- Deployment ready Docker set up
- Django -> Gunicorn -> nignx set-up
- Django -> Postgres set-up
- Django container cron job
- Tool to create and personalize docker composition settings and generate password / secret_key
- ./install_scripts/create_docker_env.py
- Docker secrets for passwords secret_key
- Docker compose file to run standalone
Before we start please be sure you either forked downloaded this template. Create and venv installed the requirements.
$ python -m venv venv
$ source ./venv/Scripts/activate
$ pip install -r ./server/requirements.txt
To start your own Rest-app we use the django framework to kick things off.
$ cd ./server
$ python manage.py startapp appointment
this will create a new package with all the necessary files. See more in this tutorial https://docs.djangoproject.com/en/5.0/intro/tutorial01/
Let us define a small model of Patients and Appointments in 'server/appointment/models.py'
from django.db import models
# Create your models here.
class Patient(models.Model):
first_name = models.CharField(max_length=64)
last_name = models.CharField(max_length=64)
birth_date = models.DateField()
def __str__(self):
return f'{self.first_name} {self.last_name} ({self.birth_date})'
class Appointment(models.Model):
date = models.DateField()
patient = models.ForeignKey(Patient, related_name="appointments", on_delete=models.CASCADE)
def __str__(self):
return f'{self.date}: {self.patient.last_name}'
Each class represents a table. The defined variables define columns on the tables. Primary-key will be automatically created, so no need to define one if nothing special is needed.
patient = models.ForeignKey(Patient, related_name="appointments", on_delete=models.CASCADE)
defines a relation ship
between the two tables.
If you have an Appointment object you can access the responding Patient object though the
attribute appointment1.patient
.
Since we also defined related_name=appointments
we can further access all responding Appointment object`
with patient1.appointments
More infos https://docs.djangoproject.com/en/5.0/topics/db/models/
With the models we can read and write data to the Database. Next we create functionalities to easily receive and respond objects to clients. Create a file server/appointment/serializers
from rest_framework import serializers
from appointment.models import Patient, Appointment
class PatientSerializer(serializers.HyperlinkedModelSerializer):
appointments = serializers.HyperlinkedRelatedField(
many=True,
read_only=True,
view_name='appointment-detail'
)
class Meta:
model = Patient
fields = ['url', 'first_name', 'last_name', 'birth_date', 'appointments']
class AppointmentSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Appointment
fields = ['url', 'date', 'patient']
Using the HyperlinkedModelSerializer
conveniently creates callable urls for the clients to request / update /
delete / use an object.
Also, we define a Meta
subclass containing the model Class to serialize and the fields which should be contained in
the respsonse.
More on https://www.django-rest-framework.org/api-guide/serializers/#hyperlinkedmodelserializer
With serializers.HyperlinkedRelatedField
we can also serialize related Appointments of the defined Person object.
The parameter view-name
defines underlying model class of the relation. The appendix '-detail'
is
django-rest-framework standard.
See more https://www.django-rest-framework.org/api-guide/relations/#hyperlinkedrelatedfield
You can add your models to the Django-Admin backend to view and manipulate the data from the administrator site. Edit ./server/appointment/admin.py
from django.contrib import admin
from appointment.models import Appointment, Patient
admin.site.register(Appointment)
admin.site.register(Patient)
Next step in our app is to create the view in ./server/appointment/views.py
from rest_framework import viewsets
from appointment.models import Patient, Appointment
from appointment.sezializers import PatientSerializer, AppointmentSerializer
class PatientViewSet(viewsets.ModelViewSet):
"""
API endpoint that allows patients to be viewed or edited.
"""
model = Patient
queryset = Patient.objects.all().order_by('last_name', 'first_name')
serializer_class = PatientSerializer
class AppointmentViewSet(viewsets.ModelViewSet):
"""
API endpoint that allows patients to be viewed or edited.
"""
model = Appointment
queryset = Appointment.objects.all().order_by('date')
serializer_class = AppointmentSerializer
Views will handle the requests. Inheriting viewsets.ModelViewSet
automatically handles all requests of a model
object.
You will be able to view all objects in the database of this class, view just a requested one or add, delete, update
objects.
model
defines the database model which will be handledqueryset
defines a database query to retrieve all objects of the class. When you use pagination it is wise to sort them.serializer_class
defines the serializer to format the object to json See more on https://www.django-rest-framework.org/api-guide/viewsets/#modelviewset
If your request is not model specific you will have plenty other possibilities. https://www.django-rest-framework.org/api-guide/views/
First we have to tell django, that we have a new app. Edit the file ./server/django_web_template and add your module to the list of installed apps.
...
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
'rest_framework_simplejwt',
'rest_framework_simplejwt.token_blacklist',
'drf_yasg',
'django_web_template.jwt_extension',
'appointment',
]
...
Please be sure you do not change anything else if you do not understand the app properly.
We now have to add our Views to the router in ./server/django_web_template/urls.py
from django.contrib import admin
from django.urls import path, include
from drf_yasg import openapi
from drf_yasg.views import get_schema_view
from rest_framework import routers, permissions
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView, TokenBlacklistView
from appointment import views as rest_views
router = routers.DefaultRouter()
router.register(r'patients', rest_views.PatientViewSet)
router.register(r'appointment', rest_views.AppointmentViewSet)
...
urlpatterns = [
path('admin/', admin.site.urls),
path('', include(router.urls)),
path('auth/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
path('auth/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
path('auth/token/blacklist/', TokenBlacklistView.as_view(), name='token_blacklist'),
path('swagger<format>/', schema_view.without_ui(cache_timeout=0), name='schema-json'),
path('swagger/', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'),
path('redoc/', schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'),
]
router.register(r'patients', rest_views.PatientViewSet)
the first parameter defines the path to access the view
https://www.django-rest-framework.org/tutorial/6-viewsets-and-routers/#using-routers
https://docs.djangoproject.com/en/5.0/topics/http/urls/
Since we added new model which will have to be created as table in the database. We have to create a migration for our app We can use django tools for this:
$ cd ./server
$ python manage.py makemigrations appointment
Migrations for 'appointment':
appointment\migrations\0001_initial.py
- Create model Patient
- Create model Appointment
This tool will automatically create a submodule in appointments called migrations
and our first migration.
With every makemigrations
we will receive a new file with the changes of our models.
Therefore, these files help us to upgrade or downgrade our database in production without loosing data.Be sure you
upload these files into github.
https://docs.djangoproject.com/en/5.0/topics/migrations/
We than can install it in our database.
$ python manage.py migrate
Operations to perform:
Apply all migrations: admin, appointment, auth, contenttypes, sessions, token_blacklist
Running migrations:
Applying appointment.0001_initial... OK
If you did not run this command yet the output will be longer
Now you can run the app as descriped above in Deployment
Before we start please be sure you either forked downloaded this template.
Create venv
$ python -m venv venv
$ source ./venv/Scripts/activate
Lets jump into the './server' path, where the actual django code is stored.
$ cd ./server
Install requirements
$ pip install -r ./requirements.txt
Django_web_template depends on environment variables. The easiest way to provide them is to copy the file ' ./server/.env.example'
$ cp .env.example .env
Now we can set-up the database. Since the '.env' file routes the database to a local sqlite file. Which will be created after installing our tables.
$ python manage.py migrate
And create a admin user
$ python manage.py createsuperuser
Username (leave blank to use 'amazing.user'): admin
Email address: [email protected]
Password:
Password (again):
This password is too short. It must contain at least 8 characters.
This password is too common.
This password is entirely numeric.
Bypass password validation and create user anyway? [y/N]: y
Superuser created successfully.
Finally, we can start django:
$ python manage.py runserver
We can check if everything is running. http://localhost:8000/admin Congrats!!
Install docker https://docs.docker.com/engine/install/. Please be aware that you need additional tools to run Docker on Windows. https://docs.docker.com/desktop/wsl/ Before we start please be sure you either forked downloaded this template.
create necessary docker environment files
$ python ./install_scripts/create_docker_env.py
Follow the instructions:
Welcome to docker environment creation
Creating .env file for docker
Define Hostname from which django will server (leave blank to use 'localhost'): localhost
Define Host port from which django will server (leave blank to use '8000'): 123456
Be sure you change the 'nginx.conf' to listen to the same port and 'docker-compose.yml' to map nginx to the same port
Define Protocol which django will server (leave blank to use 'http'): http
Postgres Database name (leave blank to use 'django'): django
Postgres Username (leave blank to use 'django'): django
Postgres Password (leave blank to generate):
.env to ..
DB pw written to ..\secrets
DB url written to ..\secrets
Secret key written to ..\secrets
Please be aware that you need to manually change the listen port in '/nginx/nginx.conf' and the mapping in /compose.yaml
server {
listen 12345;
location /static/ {
alias /var/staticfiles/;
}
location /media/ {
alias /var/mediafiles;
}
location / {
limit_req zone=mylimit burst=20 nodelay;
real_ip_header X-Forwarded-For;
real_ip_recursive on;
proxy_pass http://web_app;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Host $http_host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Port $server_port;
proxy_set_header Host $host:$server_port;
proxy_redirect off;
}
}
nginx:
image: django_server
build: ./nginx
restart: always
ports:
- ${PORT}:12345
volumes:
- static_volume:/var/staticfiles
- media_volume:/var/mediafiles
depends_on:
- web
This script will create the file .env in the root folder which general variables for the docker compose file and secrets in /secrets which are needed to securely store passwords and are then used in the compose process.
Next we build and run the composition
$ docker compose build
$ docker compose up -d
docker compose build
creates runnable images from our sources.'docker compose up
runs the images in containers.
-d is to de-attach the docker composition for the current shell process.
You can use the command docker compose up --build -d
to run in one line
We can check if everything is running. http://localhost:8000/admin Admin user and password are by default: admin 123456 This can be changed in the ./compose.yaml file Congrats!!
Let's stop it
$ docker compose down
Docker builds/downloads three images.
- 'django_proxy' which serves static files (i.e. *.css, *.js) files and routes requests to 'django_server'.
- 'django_server' runs your django app with a gunicorn wsgi.
- 'postgres' runs the postgres dbms for 'django_server' to access
$ docker image
REPOSITORY TAG IMAGE ID CREATED SIZE
django_server latest b34ab5c22521 35 minutes ago 222MB
django_proxy latest ecacf9d5acf9 2 hours ago 41.1MB
postgres latest b0b90c1d9579 4 weeks ago 425MB
With docker compose up
docker starts the images in containers.
$ docker container ls
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
e703812f3b5d django_proxy "/docker-entrypoint.…" 6 minutes ago Up 6 minutes 80/tcp, 0.0.0.0:8000->8000/tcp django_web_template-nginx-1
f70340c49b9e django_server "/bin/sh -c 'python …" 6 minutes ago Up 6 minutes django_web_template-web-1
cbb0eb1e4005 postgres "docker-entrypoint.s…" 6 minutes ago Up 6 minutes (healthy) 5432/tcp django_web_template-db-1
Also it creates a virtual network so the different containers can communicate with leaving the HOST computer.
$ docker network ls
NETWORK ID NAME DRIVER SCOPE
64e8fb510274 bridge bridge local
df52f6304d32 django_web_template_default bridge local
85c377eaa6f2 docker_gwbridge bridge local
59f5a9bac522 host host local
03a8e848ad40 none null local
Lastly it creates three virtual volumes to persistently store data.
- django_web_template_db-data: storage for postgres data files (only postgres has access)
- django_web_template_media_volume: storage for media files to store uploads (shared between django_proxy and django_server)
- django_web_template_static_volume: storage for static files. Content will be created by django_server and served by django_proxy
docker volume ls
DRIVER VOLUME NAME
local django_web_template_db-data
local django_web_template_media_volume
local django_web_template_static_volume