In this article, we'll create a project that posts about rants using Django and Django Rest Framework. We'll use Django's built-in slugify
method and override its save()
method to automatically create a slug for each rant. We'll also use a third-party package called drf-writable-nested to handle the serialization of a ManyToManyField
in the model.
We'll start by creating a virtual environment, installing the initial packages, creating the Django project, creating the Django app, and finally doing the initial migrations.
python -m venv venv
. venv/bin/activate
python -m pip install django djangorestframework
django-admin startproject myproject
cd myproject
python -m manage startapp rants
python -m manage migrate
We list rest_framework
and our rants app in the INSTALLED_APPS settings of our project and include them in the project urls.py
# settings.py
INSTALLED_APPS = [
...
'rest_framework',
'rants',
]
# myproject/urls.py
from django.urls import path, include
urlpatterns = [
path('', include('rants.urls', namespace='main')),
path('api-auth/', include('rest_framework.urls', namespace='rest_framework')),
]
Next, we create the models inside the rants
app:
# models.py
from django.db import models
class Category(models.Model):
title = models.CharField(max_length=50)
slug = models.SlugField(max_length=50)
def __str__(self):
return self.title
class Rant(models.Model):
title = models.CharField(max_length=150)
slug = models.SlugField(max_length=150)
categories = models.ManyToManyField(
Category, related_name='rants_categories')
class Meta:
verbose_name = "rant"
verbose_name_plural = 'rants'
def __str__(self):
return self.title
We have a Rant model with a title, a slug with a CharField
and a categories field with a ManyToManyField
connected to a Category model with a title and a slug field.
Then we migrate the database python -m manage makemigrations && python -m manage migrate
. Next, we create a serializer for both models:
# serializers.py
from rest_framework import serializers
from .models import Rant, Category
class CategorySerializer(serializers.ModelSerializer):
slug = serializers.SlugField(read_only=True)
class Meta:
model = Category
fields = "__all__"
class RantSerializer(serializers.ModelSerializer):
categories = CategorySerializer(many=True)
slug = serializers.SlugField(read_only=True)
class Meta:
model = Rant
fields = ('id', 'title', 'slug', 'categories')
many = True
Finally, we create our views and map them to our URLs so we can see the API endpoints of our app.
# views.py
from rest_framework.response import Response
from rest_framework.generics import ListCreateAPIView, UpdateAPIView, DestroyAPIView
from .models import Rant
from .serializers import RantSerializer
class RantList(ListCreateAPIView):
queryset = Rant.objects.all()
serializer_class = RantSerializer
def list(self, request):
queryset = self.get_queryset()
serializer = RantSerializer(queryset, many=True)
return Response(serializer.data)
class RantUpdate(UpdateAPIView):
queryset = Rant.objects.all()
serializer_class = RantSerializer
class RantDelete(DestroyAPIView):
queryset = Rant.objects.all()
serializer_class = RantSerializer
We use ListCreateAPIView
for read-write endpoints to represent a collection of model instances that provide a get
and post
method handler, UpdateAPIView
for update-only endpoints of a single model instance which provides a put
and patch
method handler, DestroyAPIView
for a delete-only endpoint of a single model instance which provides a delete
method handler. Let's map these views to the urls.py
# urls.py
from django.urls import path
from .views import RantList, RantUpdate, RantDelete
from .models import Rant
from .serializers import RantSerializer
app_name = 'rants'
urlpatterns = [
path('api/rants/', RantList.as_view(queryset=Rant.objects.all(), serializer_class=RantSerializer)),
path('api/rants/update/<int:pk>/', RantUpdate.as_view(queryset=Rant.objects.all(), serializer_class=RantSerializer)),
path('api/rants/delete/<int:pk>/', RantDelete.as_view(queryset=Rant.objects.all(), serializer_class=RantSerializer)),
]
We can now view the API endpoints in the browser using the drf package, but I personally prefer to see the API endpoints using another package which is the drf-yasg
package. Let's install and configure the package:
python -m pip install drf-yasg
# settings.py
INSTALLED_APPS = [
....
'rest_framework',
'drf_yasg',
]
# urls.py
from django.urls import path, include
from rest_framework import permissions
from drf_yasg.views import get_schema_view
from drf_yasg import openapi
schema_view = get_schema_view(
openapi.Info(
title="Rants API",
default_version='v1',
description="Rants",
terms_of_service="https://www.google.com/policies/terms/",
contact=openapi.Contact(email="[email protected]"),
license=openapi.License(name="BSD License"),
),
public=True,
permission_classes=[permissions.AllowAny],
)
urlpatterns = [path('api/', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'),]
Now we run python -m manage runserver
and head over to http://localhost:8000/api/ to see what we've created
But we have a problem; the categories field has an error despite inputting a str
in the field.
{
"categories": [
"Expected a list of items but got type \"str\"."
]
}
To solve this, we'll install drf-nested-writable
in our app to serialize the categories field and then update the serializers.py
file to include WritableNestedModelSerializer
in our RantSerializer
python -m pip install drf-nested-writable
# serializers.py
from drf_writable_nested.serializers import WritableNestedModelSerializer
class RantSerializer(WritableNestedModelSerializer):
categories = CategorySerializer(many=True)
slug = serializers.SlugField(read_only=True)
class Meta:
model = Rant
fields = ('id', 'title', 'slug', 'categories')
many = True
Now when we add new data for our app using the rants_create
endpoint, we won't get the error we got above anymore but instead. We also override the save()
method in our models for the slug
field to automatically fill the database in
The code to override the save()
method in our models
# models.py
...
from django.utils.text import slugify
...
def save(self, *args, **kwargs):
self.slug = slugify(self.title)
super(Category, self).save(*args, **kwargs)
return self.slug
...
def save(self, *args, **kwargs):
self.slug = slugify(self.title)
super(Rant, self).save(*args, **kwargs)
return self.slug
...
Conclusion: This took me a while to solve since I'm at GMT+8, but hey, at least it got solved, and I learned how to override the save method in the models and learned about the drf-writable-nested package to fix my issue.
Also published here.