- Set up project
- create environment
- install django 4.0 (long-term support version)
- install cloudinary to handle static files
- install Pillow to handle image processing
- add a profiles app
- use django signals
- signals explained
- create a superuser
- display a model on the admin panel
- add a basic serializer to the profiles app
- install rest_framework
- how to manually add a put request on a view
- add a detail view to the profiles app
- how to make the update form match the serializer
- info on how to use dot notation to traverse related fields in databases in serializers
- serializing new fields onto a view using serializer method fields
- adding authentication / logging in and out
- adding custom authentication and restrictions to data
- create the post app
- create the model
- create the list view
- create the serializer
- add authentication and ability to post as an authenticated user
- create the PostDetail view
- retrieve a record by id (get)
- update a record by id (put)
- delete a record by id (delete)
- setting up the entire comments app using previous methods
- views made using generic views instead
- setting up the likes app using generic views
- using unique constraints to make likes unique/prevent duplicates
- creating the entire followers app using previous methods
- refactor the posts and profiles views
- adding axtra fields to posts and profiles, Part 1
- creating link to multiple entries on a single entry on a seperate table using the ManyToMany Field
- adding extra fields to the posts and profiles, part 2
- adding custom filters
- additional related data display methods outside of the serializer
- create additional fields for the posts app
- use previously used methods to create comments and likes counts on a post entry
- adding a search feature to an api app (posts, in this case)
- add a filter feature to filter the API data
- posts owned by a user
- posts liked by a user
- posts by another user that a user us following
- filter user list of accounts a user is following
- using django_filters library
- implementing JWT Token authentication
- create a new repo on github
- or, use the Codeinstitute full template: https://github.com/Code-Institute-Org/ci-full-template
- install Django via the CLI
pip3 install django
- for this walkthrough, the LTS (Long Term Support) version of cjango was used. to use this, use:
pip3 install 'django<4'
- Start a new project in the repo with django called
drf_api
, using the following command:django-admin startproject drf_api .
- the
.
initializes the project in the current directory
- the
- next, install cloudinary into the project with the following command:
pip install django-cloudinary-storage
- install
Pillow
.- this library adds image processing capabilities that we need for this project. Note that it’s name starts with a capital P.
pip install Pillow
- import the newly installed cloudinary apps (
cloudinary_storage
andcloudinary
) intosettings.py
>INSTALLED_APPS[]
- be sure to place
cloudinary_storage
BEFOREdjango.contrib.staticfiles
- add
cloudinary
to the bottom of theINSTALLED_APPS
list. - example of what
INSTALLED_APPS
should look like:INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'cloudinary_storage', 'django.contrib.staticfiles', 'cloudinary' ]
- be sure to place
- set up
env.py
and the cloudinary environment variable- create a new
env.py
file - make sure it is included in
.gitignore
- add
import os
to the top of the file - set an environ for cloudinary using the cloudinary API key found on your cloudinary account page
os.environ['CLOUDINARY_URL'] = 'cloudinary://[REMOVE_THIS_AND_ADD_YOUR_API_KEY]@[REMOVE_THIS_AND_ADD_YOUR_CLOUD_NAME]'
- make an offline copy of your
env.py
file, incase you have to make a fresh workspace
- create a new
- next, set up a conditional in
settings.py
that ifenv.py
exists, import it into settings.py- place this directly below
from pathlib import Path
at the tp of the file -
import os if os.path.exists('env.py'): import env
- place this directly below
- beneath that code, retreive the cloudinary environment as an object inside a variable called
CLOUDINARY_STORAGE
:CLOUDINARY_STORAGE = {'CLOUDINARY_URL': os.environ.get('CLOUDINARY_URL')}
- then, beneath that, define a setting for where media files will be stored:
MEDIA_URL = '/media/'
- lastly, beneath that, add a default file storage setting:
DEFAULT_FILE_STORAGE = 'cloudinary_storage.storage.MediaCloudinaryStorage'
top of setting.py
should now look like this:
from pathlib import Path
import os
if os.path.exists('env.py'):
import env
CLOUDINARY_STORAGE = {'CLOUDINARY_URL': os.environ.get('CLOUDINARY_URL')}
MEDIA_URL = '/media/'
DEFAULT_FILE_STORAGE = 'cloudinary_storage.storage.MediaCloudinaryStorage'
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'SECRET_KEY_REMOVED_LOL'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = []
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'cloudinary_storage',
'django.contrib.staticfiles',
'cloudinary'
]
- create the new app with the
startapp
command:python3 manage.py startapp profiles
- (
python3 manage.py startapp YOUR_APP_NAME_HERE
)
- (
- with the new app created, it also needs to be included in
INSTALLED_APPS
insettings.py
:-
INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'cloudinary_storage', 'django.contrib.staticfiles', 'cloudinary', 'profiles', ]
-
- now, in the newly created
models.py
insideprofiles.py
add a new import at the top of the file:from django.contrib.auth.models import User
- next, inside
models.py
create the first new database model:-
class Profile(models.Model): owner = models.OneToOneField(User, on_delete=models.CASCADE) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) name = models.CharField(max_length=255, blank=True) content = models.TextField(blank=True) image = models.ImageField( upload_to='images/' default='../samples/landscapes/girl-urban-view' ) class Meta: ordering = ['-created_at'] def __str__(self): return f"{self.owner}'s profile"
-
- event notifications
You can think of signals as notifications that get triggered by an event.
- can listen to the events and run a piece of code every time
We can listen for such Model events and have some code, usually a function, run each time that signal is received.
- we'd like to create a user profile every time a user is created
In our case, we would want to be notified when a user is created so that a profile can automatically be created alongside it.
- built-in model signals:
- pre save:
pre_save
- post save:
post_save
- pre delete:
pre_delete
- post delete
post_delete
- pre save:
to implement:
- import the
post_save
signal fromdjango.db.models.signals
at the top ofmodels.py
from django.db.models.signals import post_save
- now, beneath the
Profile
class model, add a call topost_save
appended with the.connect()
function, passing 2 arguments:- the first argument will be
create_profile
, which is the function that needs to be run every time a post is saved. - the second is going to be specifiying who/what is the sender of the new data, in this case, it will be the
User
post_save.connect(create_profile, sender=User)
- the first argument will be
- now, above this call, define the
create_profile
function:- the function has to take 4 arguments:
(sender, intance, created, **kwargs)
- set a conditional statement that if a profile is created, the owner of the profile should be that
User
- the function should look like this:
-
def create_profile(sender, instance, created, **kwargs): # Because we are passing this function # to the post_save.connect method # it requires the following arguments: # 1. the sender model, # 2. its instance # 3. created - which is a boolean value of # whether or not the instance has just been created # 4. and kwargs. if created: # if created is True, we’ll create a profile # whose owner is going to be that user. Profile.objects.create(owner=instance) # not part of the function, but this would be sitting # directly under it post_save.connect(create_profile, sender=User)
now every time a user is created, a signal will trigger the Profile model to be created.
- the function has to take 4 arguments:
- register the
Profile
model inadmin.py
so that it will show up in the admin paneladmin.site.register(Profile)
- before going any further, migrate the new model into the database in the CLI:
python3 manage.py makemigrations
python3 manage.py migrate
- now, to view the model on the admin panel:
- first create a superuser/admin
python3 manage.py createsuperuser
- create a username and password from the command prompts
- no need to supply an email address
- first create a superuser/admin
- run the server and go to admin and see if everything is working:
python3 manage.py runserver
- append
/admin
to the url in the browser window and login with the admin credentials - be sure to add the workspcae url as an
ALLOWED_HOST
insettings.py
Now, if we run our server, and go to /admin, we see that our first user was created. And their corresponding profile was created with a working image, well done!
- finally, create a file containing the new project's dependencies via the CLI
pip freeze > requirements.txt
- push the changes to github
- how to write a class view to list all profiles
- how to write a ProfileList
- how to write an APIView
- Learn what Serializers are and why they're useful
- what the similarities are between ModelForms and ModelSerializers
- how to write a profile model serializer
How Model Serializers work:
- similar to Django's
ModelForms
, they handle validation - Uses similar syntax to
ModelForms
:- can use
Meta
class - can specify extra fields.
- can use
.is_valid()
and.save()
methods - Handle all the conversions between different data types
- can use
we need a serializer to convert Django model instances to JSON. As we’ll only be working with Django Models, we’ll use model serializers to avoid data replication, just like you would use ModelForm over a regular Form. Before we write a model serializer, let’s talk a bit more about what they are: they are very similar to Django’s ModelForms, in that, they handle validation. The syntax for writing a model serializer is the same, we specify the model and fields in the Meta class and we can specify extra fields too. We can use methods like .is_valid and .save with serializers Additionally, they handle all the conversions between different data types.
-
install
djangorestframework
in the CLIpip3 install djangorestframework
- add it to
INSTALLED_APPS
insettings.py
belowcloudinary
, but above any manually created apps likeProfiles
'rest_framework',
-
INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'cloudinary_storage', 'django.contrib.staticfiles', 'cloudinary', 'rest_framework', 'profiles' ]
-
create the first view in
views.py
, start by importing everything needed- import
APIView
fromrest_framework.views
at the top ofviews.py
from rest_framework.views import APIView
-
APIView is very similar to Django’s View class. It also provides a few bits of extra functionality such as making sure you receive Request instances in your view, handling parsing errors, and adding context to Response objects.
- import
Response
fromrest_framework.response
from rest_framework.response import Response
-
Even though we could use Django’s HttpResponse, the Response class is specifically built for the rest framework, and provides a nicer interface for returning content-negotiated Web API responses that can be rendered to multiple formats.
- import the
Profile
database model frommodels.py
from .models import Profile
- import
-
next, use the imported
APIView
view to create a class view calledProfileList
- inside it, define a get request that puts all objects in the
Profiles
table into a variable calledprofiles
- then, in the return statement, return a
Response
with the parameter of theprofiles
variable -
class ProfileList(APIView): def get(self, request): profiles = Profile.objects.all() return Response(profiles)
- inside it, define a get request that puts all objects in the
-
map the new view to a URL in
urls.py
in theprofiles
app- first, import
path
fromdjango.urls
- import the
profiles
argument established in thereturn Response
fromviews.py
-
from django.urls import path from profiles import views
-
- next, create the
urlpatterns
list variable, and write a path forprofiles/
that renders theProfileList
view as a view (as_view()
)-
urlpatterns = [ path('profiles/', views.ProfileList.as_view()), ]
-
- first, import
-
map the newly made url pattern in
profiles/urls.py
into the urls ofdrf_api
- at the top of
drf_api/urls.py
addinclude
to the imports fromdjango.urls
from django.urls import path, include
- use the newly imported
include
method to write in a new path forprofiles.urls
indrf_api/urls.py
'surlpatterns
-
from django.contrib import admin from django.urls import path, include urlpatterns = [ path('admin/', admin.site.urls), path('', include('profiles.urls')), ]
-
Now, if we start the server, and go to ‘profiles/’, we get an ugly error: Object of type Profile is not JSON serializable. Let's have a look at what’s happening and why we’re seeing this error.
When a user posts data to our API, the following has to happen: we need that data to be deserialized, which means it needs to be converted from a data format like JSON or XML to Python native data types.
Then, we have to make sure the data is valid (just like with Django forms) once it’s validated, a model instance is saved in the database.
If our users are requesting data from our API, a queryset or model instance is returned from the database. It is then converted to Python native data types before the data is sent back, it is converted again, or serialized to a given format (most commonly JSON).
This is the reason we saw the error. The profiles can’t be just thrown in as a part of the Response, we need a serializer to convert Django model instances to JSON.
- at the top of
-
create a new file,
serializers.py
in theprofiles
app. -
in the new file, import the following:
serializers
fromrest_framework
Profile
from.models
-
from rest_framework import serializers from .models import Profile ```
-
create a class with the name
ProfileSerializer
that inherits fromserializers.ModelSerializer
and create aReadOnlyField
fromserializers
calledowner
, it should contain the parametersource='owner.user
- give it a
Meta
class and specify the following fields to include in the responsemodel = Profile
fields = []
- the value for fields can be determined in 2 ways:
-
You could list them all in an array or set to ‘all’ like this, however I prefer to be explicit about which fields I am including, because I might want to add another field to my Profile model later on, that I don’t want included in the serializer.
fields = '__all__'
fields = ['id', 'owner', 'created_at', 'updated_at', 'name', 'content', 'image']
-
Please note, that when extending Django's model class using models.models, the id field is created automatically without us having to write it ourselves. If we want it to be included in the response, we have to add it to the serializer's field array.
-
-
from rest_framework import serializers from .models import Profile class ProfileSerializer(serializers.ModelSerializer): owner = serializers.ReadOnlyField(source='owner.username') class Meta: model = Profile fields = [ 'id', 'owner', 'created_at', 'updated_at', 'name', 'content', 'image' ]
- give it a
-
return to
views.py
and import the newly madeProfileSerializer
classfrom .serializers import ProfileSerializer
-
then, in the
ProfileList
class inviews.py
, add a new instance calledserializer
, which makes a call to the newly importedProfileSerializer
class at the top of the file, it should contain 2 arguments:- first should be the
profiles
variable containingall
theProfile
objects
- second, should be
many=True
to specify that multiple profile instances are to be serialized
- first should be the
-
in the return statement of
ProfileList
, update theresponse
parameter with the value ofserializer.data
-
from django.shortcuts import render from rest_framework.views import APIView from rest_framework.response import Response from .models import Profile from .serializers import ProfileSerializer class ProfileList(APIView): def get(self, request): profiles = Profile.objects.all() serializer = ProfileSerializer(profiles, many=True) # this takes the objects in profiles and runs it through the # ProfileSerializer defined in serializers.py # it takes this data, then takes the `Profile` model as a reference # and then renders the specified fields within the passed-in # dataset into JSON data return Response(serializer.data)
-
-
check this is all working by running the preview and appending the preview url with
/profiles/
python3 manage.py runserver
- if running correctly, a django REST page should appear, displaying JSON data objects of all the profiles existing within the app.
-
we can see the array being returned in a nice user interface created by rest framework. If we click on the GET json button, we’ll see plain JSON, which is exactly what a React application would see. Our serializer has taken the Python data and converted it into JSON, which is ready for the front end content to use!
-
Before we finish, let’s update our dependencies.
- update
requirements.txt
in the terminalpip3 freeze > requirements.txt
- update
- The
source
attribute and its string value explained - how to manipulate ReadOnlyFields (using dot notation)
- targeting subfields within data models / tables
Entity relationship between the User
table that comes with Django, and the Profile
table created in this project:
The User and Profile tables are connected through the owner OneToOne field.
By default, the owner field always returns the user’s id value.
US | ER | < - > | PRO | FILE |
---|---|---|---|---|
id | BigAuto | |||
id | BigAuto | -- > | owner | OneToOne |
username | Char | name | Char | |
password | Char | content | Text | |
image | Image |
For readability’s sake, however, every time we fetch a profile, it makes sense to overwrite this default behaviour and retrieve the user’s username instead.
US | ER | < - > | PRO | FILE |
---|---|---|---|---|
id | BigAuto | id | BigAuto | |
username | Char | -- > | owner | OneToOne |
password | Char | name | Char | |
content | Text | |||
image | Image |
To access this field, we use dot notation.
inside serializers.py, in the ProfileSerializer
class
owner = serializers.ReadOnlyField(
source='owner.username'
)
In this case, the “owner” in “owner.username” stands for the user instance, so any time we want to access a field we simply use dot notation.
basically owner
can act as a direct call to that specific user, and using dot notation from owner
would be the same as User.AWAYTOIDENTIFYASPECIFICUSER.WHATEVERFIELD
Now, lets add a 3rd table for posts
US | ER | < - > | PO | ST | < - > | PRO | FILE |
---|---|---|---|---|---|---|---|
id | BigAuto | id | BigAuto | id | BigAuto | ||
username | Char | < - > | owner | Foreign Key | < - > | owner | OneToOne |
password | Char | title | Char | name | Char | ||
content | Text | content | Text | ||||
image | Image | image | Image |
Let’s assume we are working on PostSerializer instead of ProfileSerializer like we did in the previous video.
In these tables, Post.owner
is a ForeignKey
field, this means if the there was a serializer serializing information from Post
, the name of a user could be found by dot notation, by going through owner
(which is targeting the user through it being na instance) then Profile
, so the dot notation would be owner.Profile.name
.
class PostSerializer(serializers.ModelSerializer):
profile_image = serializers.ReadOnlyField(
source='owner.profile.name'
)
One last challenge. Let’s say we wanted to add the profile image field to each post.
US | ER | < - > | PO | ST | < - > | PRO | FILE | subfields |
---|---|---|---|---|---|---|---|---|
id | BigAuto | id | BigAuto | id | BigAuto | |||
username | Char | < - > | owner | Foreign Key | < - > | owner | OneToOne | |
password | Char | title | Char | name | Char | |||
content | Text | content | Text | |||||
image | Image | image | Image | url |
Because the profile.image
field comtains a "subfield" housing the url
for the image, that needs to be targeted directly in the dot notation.
class PostSerializer(serializers.ModelSerializer):
profile_image = serializers.ReadOnlyField(
source='owner.profile.image.url'
)
Profiles CRUD Table:
Let’s have a look at our CRUD table. It shows what kind of HTTP request our users have to make and to which URL in order to list all profiles, create, read, update or delete the profile.
HTTP | URI | CRUD Operation | View Name |
---|---|---|---|
a.GET / b.POST | /profiles | a.list all profiles / b.create a profile | LIST |
c.GET / d.PUT / e.DELETE | /profiles/:id | c.retrieve a profile by id / d.update a profile by id / e.delete a profile by id | DETAIL |
quick recap reference to the ProfileList view to help write the next code:
class ProfileList(APIView):
"""
List All profiles
No Create view (POST method), as profile creation is
handles by Django signals
"""
def get(self, request):
profiles = profile.objects.all()
serializer = ProfileSerializer(profles, many=True)
return Response(serializer.data)
while this class handles GET
ting a list of all profiles, it doesn't POST
data/create new profiles.
This is because profile creation is handled by django signals, referenced earlier.
there is still no:
GET
method to retreive a specific profile by id- view or methods to utulise
PUT
, which updates data (a profile) by id
the view needs to:
- fetch the profile by id
- serialize the profile model instance
- return serializer data in the response
- (serializer needed to turn the data into JSON format)
steps:
- Go to
views.py
- create a new class called
ProfileDetail
that inherits from the importedAPIView
- create a
get_object
request that takesself
andpk
(PrimaryKey) as arguments- this function not only has to try and retreive a data record by the parameters above, it also needs to handle the instance of when a record doesnt exist
- because of this, add an except/exception block by using the
try
keyword - inside the block, add a variable called profile that queries the
objects
ofProfile
toget
a record wherepk
= thepk
entered in the request. -
class ProfileDetail(APIView): def get_object(self, pk): try: profile = Profile.objects.get(pk=pk) return profile
- this handles the instance of where the record exists, now the code needed to handle the exception needs to be added
- Create the code that handles the event of a record not existing
- at the top of the file (
views.py
), importHttp404
fromdjango.http
from django.http import Http404
- then, back in the
ProfileDetail
class, add anexcept
statement to thetry
statement.- this is structured like
if
/else
- the
except
ion should handle the event ofProfile.DoesNotExist
- the action it takes in this event is to
raise
a call toHttp404
-
class ProfileDetail(APIView): def get_object(self, pk): try: profile = Profile.objects.get(pk=pk) return profile except Profile.DoesNotExist: raise Http404
- this is structured like
- at the top of the file (
- now that the records have been checked to make sure the profile exists, the queried profile now needs to be serialized:
- beneath the
get_object
request, write a newget
request that takesself
, therequest
, andpk
(primary key) as arguments - create a variable called
profile
with the value being theget_object
function declared above, being called on itself
, taking thepk
as the argumentprofile = self.get_object(pk)
- create a variable called
serializer
, its value should be a call to theProfileSerializer
with theprofile
variable as its argument-
unlike in
ProfileList
No need to pass in many=True, as unlike last time, we’re dealing with a single profile model instance and not a queryset.
-
- with the profile now serialized, return it in the
Response
for theget
request: -
class ProfileDetail(APIView): def get_object(self, pk): try: profile = Profile.objects.get(pk=pk) return profile except Profile.DoesNotExist: raise Http404 def get(self, request, pk): profile = self.get_object(pk) serializer = ProfileSerializer(profile) return Response(serializer.data)
- beneath the
- test that this code is working by adding the
DetailView
to theurlpatterns
inurls.py
:- under the list view, add a new
path
that takes the arguments of:- the path for the url as a string value that calls the the
profiles
path, but then in<>
thepk
as anint
eger, suffixed with a/
'profiles/<int:pk>/'
- the
ProfileDetail
view fromviews
as a view, usingas_view()
views.ProfileDetail.as_view()
- the path for the url as a string value that calls the the
-
urlpatterns = [ path('profiles/', views.ProfileList.as_view()), path('profiles/<int:pk>/', views.ProfileDetail.as_view()) ]
- run the app to see if it all works.
python3 manage.py runserver
- under the list view, add a new
process:
- fetch the profile by it's id
-
First, we’ll have to retrieve the profile using the get_object method.
-
- call the serializer with the profile and request data
-
Next, we’ll have to call our serializer with that instance and data that’s being sent in the request.
-
- if data is valid, save and return the instance
-
Then, we’ll call .is_valid on our serializer, just like we would on a form, to make sure the data is valid. If it is, we’ll save the updated profile to the database and return it in the Response. In case it isn’t, we’ll have to return a 400 BAD REQUEST error.
-
steps:
- inside the
profileDetail
class inviews.py
, define aput
request that takesself
, therequest
and thepk
(PrimaryKey) as arguments- define the
profile
variable again like in theget
request, where the variable's value is running theget_object
function on itself
, taking thepk
as an argument - define the serializer variable again using the
ProfileSerializer
as its value, this time though, it should take 2 arguments:profile
data=request.data
which is taking the form data from the request passed in
- now check
if
the value ofserializer
is_valid()
, with anif statement
, if it is,save()
serializer
return
aResponse
with the parameter of theserializer
'sdata
- instead of an
else
statement, add areturn
statement in place of anelse
statement, whichreturn
s aResponse
with 2 arguments:- any
errors
created by theserializer
- a
status
parameter imported fromrest_framework
, that has the value ofstatus.HTTP_400_BAD_REQUEST
- be sure to -
from rest_framework import status
- at the top ofviews.py
- be sure to -
- any
-
def put(self, request, pk): profile = self.get_object(pk) serializer = ProfileSerializer(profile, data=request.data) if serializer.is_valid(): serializer.save() return Response(serializer.data) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
- define the
Ok, that's all! Let’s make sure our new view is working! If we go to ‘profiles/id/’, we’ll see PUT among the allowed HTTP methods. We’ll also have a text area in which we could write raw JSON to update the profile. But wouldn’t it be better to have a nice form instead?
- make the newly created form in the ProfileDetail view contextual by setting the
serializer_class
toProfileSerializer
in theProfileDetail
class, as the first line under the class definition:- this tells the
APIView
template being used byProfileDetail
to follow the field structure of theProfileSerializer
for its forms -
class ProfileDetail(APIView): # establish the form structure for the data serializer_class = ProfileSerializer def get_object(self, pk): """ the function that checks the validity of a profile request, returns an error if invalid """ try: profile = Profile.objects.get(pk=pk) return profile except Profile.DoesNotExist: raise Http404 def get(self, request, pk): """ uses the function above to get a profile by id serializes it using the ProfileSerializer """ profile = self.get_object(pk) serializer = ProfileSerializer(profile) return Response(serializer.data) def put(self, request, pk): """ updates a retreived profile with data receieved from a request via a form contextualised by serializer_class at the top of this view handles BAD_REQUEST errors too """ profile = self.get_object(pk) serializer = ProfileSerializer(profile, data=request.data) if serializer.is_valid(): serializer.save() return Response(serializer.data) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
- this tells the
This section covers:
- add in-browser login/logout feature
-
make it possible to log in and log out of our API in-browser interface
-
- write custom permissions
-
write the IsOwnerOrReadOnly permission
-
- add an extra field to an existing serailizer
-
add an extra field to our Profile Serializer
-
We’ll do all this so that only the owner of a profile can edit it and not just any user.
before getting started on the steps above, create another superuser, so that there are 2 accounts to work with:
- in the terminal:
python3 manage.py createsuperuser
- follow terminal prompts
user created:
Username | Password |
---|---|
admin2 | guest |
with that done, begin creating the login/logout views:
These come as part of the native REST framework package:
- add a new path to
drf_api/urls.py
that takesapi-auth/
as the path name, and uses theinclude
import to include'rest_framework.urls'
-
from django.contrib import admin from django.urls import path, include urlpatterns = [ path('admin/', admin.site.urls), path('api-auth/', include('rest_framework.urls')), path('', include('profiles.urls')), ]
-
- run the server and check to see if the new path works:
- on the
/profiles/
page, there should now be a dorpdown in the top right of the page allowin users to logout/login
- on the
however, regardless of who is logged in, any user can update a record, which is not ideal.
Luckily, not only does the rest framework come with a set of commonly used permissions, like
AllowAny
,IsAuthenticated
,IsAdminUser
,IsAuthenticatedOrReadOnly
and more; it also makes it very easy to write custom permissions (BasePermission
- used to write custom permissions)
requirements for a custom permission:
-
It has to be an object-level permission, which means we’ll have to check a Profile model instance object and see if its ‘owner’ field is pointing to the same user who’s making the request.
-
If the user is requesting read-only access using the so-called safe http methods, like GET, return True.
-
If the user is making a PUT or PATCH request, return True only if that user owns the profile object.
steps:
- create a new file in
drf_api
calledpermissions.py
- at the top of the file, import
permissions
fromrest_framework
from rest_framework import permissions
- then, create a new class called
IsOwnerOrReadOnly
that inherits frompermisssions.BasePermission
class IsOwnerOrReadOnly(permissions.BasePermissions):
- overwrite the pre-existing function
has_object_permission
supplied by the inherited import. it takes 4 arguments(self, request, view, obj)
- this is done by just writing it as a new function
- in the function:
- add an
if
statement that checks if themethod
in therequest
argument isin
theSAFE_METHODS
ofpermissions
. If it is,return
a value ofTrue
- this checks to see if the user accessing the record is only requetsing Read-Only access, and if they are, the
True
result is returned
- this checks to see if the user accessing the record is only requetsing Read-Only access, and if they are, the
- directly under it, in lieu of an
else
statement, add areturn
statement that has a conditional checking thatowner
in theobj
argument matches therequest
inguser
- this checks to see if the user owns that record and will return a
True
value if they match.
- this checks to see if the user owns that record and will return a
-
class IsOwnerOrReadOnly(permissions.BasePermisssions): def has_object_permission(self, request, view, obj): if request.method in permissions.SAFE_METHODS: return True return obj.owner == request.user
- add an
- with the permission written, it can now be used in
views.py
, import it at the top of theviews.py file
:from drf_api.permissions import IsOwnerOrReadOnly
- now that it is imported, permissions classes can be added to views by using the variable
permission_classes
which passes an array of all of the necessary permissions. In this case, there is only one, so pass it in as a single entry within an array/list. Add the following variable to theProfileDetail
view, below theserializer_class
variable:permission_classes = [IsOwnerOrReadOnly]
- next, inside the
ProfileDetail
view, theget_object
function now needs to be amended to make the function check the permissions of the accessing user:-
if the user doesn’t own the profile, it will throw the 403 Forbidden error and not return the instance.
- in the
try
statement, beneath the profile variable, run the functioncheck_object_permissions
onself
with the arguments ofrequest
made byself
and theprofile
variable established directly above itself.check_object_permissions(self.request, profile)
-
class ProfileDetail(APIView): # establish the form structure for the data serializer_class = ProfileSerializer # establish the permissions for the data permission_classes = [IsOwnerOrReadOnly] def get_object(self, pk): """ the function that checks the validity of a profile request, returns an error if invalid """ try: profile = Profile.objects.get(pk=pk) self.check_object_permissions(self.request, profile) return profile except Profile.DoesNotExist: raise Http404 def get(self, request, pk):
-
now, if we don’t own the profile, we aren’t allowed to make changes to it.
- go to
profiles/serializers.py
-
We’re going to use the SerializerMethodField, which is read-only. It gets its value by calling a method on the serializer class, named
get_<field_name>
(in this case, it will beget_is_owner
).
-
- in the
ProfileSerializer
class, add a new variable calledis_owner
under theowner
variable- this variable will house the
SerializerMethodField
from theserializers
import with no parameters.is_owner = serializers.SerializerMethodField()
SerializerMethodField()
is read-only
- this variable will house the
- this new variable is then run like a function by prefixing the variable name with
get_
and defining it like a function, it will takeself
andobj
as parameters
we’d like to do something similar to what we did in our permission file, that is: check if request.user is the same as the object's owner. But there’s a problem. The currently logged in user is a part of the request object.
This information isn't currently directly available to the serializer in this file. So it needs to be passed into the serializer from views.py
inside views.py, we’ll have to pass it in as part of the context object when we call our ProfileSerializer inside our view. We’ll have to do it every time we call it.
- first, add a new variable into the
get_is_owner
function calledrequest
, its value should beself
'scontext
, where it targets the array value of'request'
within itself- this
context
needs to be created as a parameter when the serializer is called inviews.py
, but this can be added after the rest of the function is built to save jumping back and forth
- this
- with the
request
variable now housing thecontext
ualrequest
from theuser
, this can be checked against the target object to see if theuser
matches theobj
'sowner
- under the
request
write areturn
statement that has a conditional statement to reflect this:-
def get_is_owner(self, obj): request = self.context['request'] return request.user == obj.owner
-
- under the
- now, as mentioned before, the request information needs to be sent in the
context
value fromviews.py
every time this serializer is called. to do that, go toviews.py
- wherever
serializer = ProfileSerializer(...)
is classed as a variable, a parameter ofcontext
needs to be added to the parameters, its value needs to be an object KVP with a key of'request'
with the value ofrequest
serializer = ProfileSerializer(profiles, many=True, context={'request': request})
(for accessing multiple records)serializer = ProfileSerializer(profiles, context={'request': request})
(for accessing a single record)- example:
-
def put(self, request, pk): """ updates a retreived profile with data receieved from a request via a form contextualised by serializer_class at the top of this view handles BAD_REQUEST errors too """ profile = self.get_object(pk) # serailizer updated below serializer = ProfileSerializer( profile, data=request.data, context={'request': request} ) if serializer.is_valid(): serializer.save() return Response(serializer.data) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
-
- Lastly, include the new
'is_owner'
field in thefields
array listed in theMeta
class ofProfileSerializer
- test that it all works
code should look like this:
from rest_framework import serializers
from .models import Profile
class ProfileSerializer(serializers.ModelSerializer):
owner = serializers.ReadOnlyField(source='owner.username')
is_owner = serializers.SerializerMethodField()
# this variable above is used to house
# the requisite serializer it is called
# as a function below by prefixing the variable's
# name with 'get_'
def get_is_owner(self, obj):
"""
passes the request of a user into the serializer
from views.py
to check if the user is the owner of a record
"""
request = self.context['request']
return request.user == obj.owner
class Meta:
model = Profile
fields = [
'id',
'owner',
'created_at',
'updated_at',
'name',
'content',
'image',
'is_owner'
]
run the command in the terminal to make a new app, name it posts
- python3 manage.py startapp posts
-
in
posts/models.py
import the following:models
fromdjango.db
User
fromdjango.contrib.auth.models
from django.db import models from django.contrib.auth.models import User
-
create a class called
Post
, it should inherit frommodels.Model
-
the new
Post
class needs the following fields:- owner, which should be a
ForeignKey
using theUser
as its value, it should alsoCASCADE
delete any related sub-items if deleted - created_at, which should be a
DateTimeField
which should be automatically assigned a new value upon record creation by using the parameterauto_now_add=True
- updated_at, same as above, except its paramater is
auto_now=True
, notauto_add_now
as it updates every time the post is edited, not when it isadd
ed - title, should be a
CharField
with amax_length
- content, should be a
TextField
to house post content, it should beblank
initially - image, which should be an
ImageField
.- upon a successful upload, the image should be
upload
ed_to
'images/'
- it should also have a default value from the cloudinary database using a static filepath
- it should also be
blank
on the form
- upon a successful upload, the image should be
- owner, which should be a
-
add a
Meta
class that orders posts by theDateTime
they werecreated_at
, with the most recent entry being first -
define a dunder
str
methos that returns a pythonf
string that contains theid
andtitle
of each post through the use ofthis
the post model should look like this:
from django.db import models
from django.contrib.auth.models import User
class Post(models.Model):
"""
Post model, related to 'owner', i.e. a User instance.
Default image set so that we can always reference image.url.
"""
owner = models.ForeignKey(User, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
title = models.CharField(max_length=255)
content = models.TextField(blank=True)
image = models.ImageField(
upload_to='images/', default='../samples/landscapes/girl-urban-view', blank=True
)
class Meta:
ordering = ['-created_at']
def __str__(self):
return f'{self.id} {self.title}'
- Migrate the completed model into the database
python3 manage.py makemigrations
python3 manage.py migrate
- create a new file in
posts
calledserializers.py
- inside the new
serailizers
import:serializers
fromrest_framework
- the
Post
model from.models
- create a class called
PostSerializer
which inherits fromserializers.ModelSerializer
- establish the following serailizer fields:
- owner: a
ReadOnlyField
that'ssource
is theusername
of theowner
specified in thePost
model - profile_id: a
ReadOnlyField
that'ssource
is theid
of theowner
specified in thePost
model (id
's are automatically created by django) - profile_image: a
ReadOnlyField
that'ssource
is theurl
of theimage
field beonging to theowner
, specified in thePost
model - is_owner: a variable which houses the
SerializerMethodField
for theserializer
- owner: a
- use the
is_owner
field to create a method (prefix it withget_
) with the following parameters:self
andobj
for object in question- the function should then take a
context
ualrequest
from whatever calls it (this
), and then checks if theuser
that made therequest
matches theobj
'sowner
,return
ing a voolean value depending on the outcome
- the function should then take a
- add a
Meta
class that:- determines the
model
the serializer its basing its structure from, n this case it isPost
- determine the
fields
it serializes and displays in a list variable as strings, they should be:- id
- owner
- created_at
- updated_at
- title
- content
- image
- determines the
finished serializer shuold look like this:
from rest_framework import serializers
from .models import Post
class PostSerializer(serializers.ModelSerializer):
owner = serializers.ReadOnlyField(source='owner.username')
profile_id = serializers.ReadOnlyField(source='owner.id')
profile_image = serializers.ReadOnlyField(source='owner.image.url')
is_owner = serializers.SerializerMethodField()
# this variable above is used to house
# the requisite serializer it is called
# as a function below by prefixing the variable's
# name with 'get_'
def get_is_owner(self, obj):
"""
passes the request of a user into the serializer
from views.py
to check if the user is the owner of a record
"""
request = self.context['request']
return request.user == obj.owner
class Meta:
model = Post
fields = [
'id',
'owner',
'profile_id',
'profile_image',
'created_at',
'updated_at',
'title',
'content',
'image',
'image_filter',
'is_owner',
]
overview:
- create the PostList view and write two methods
- ‘get’, to list all posts
- ‘post’ to create a user post
- go to
posts/views.py
- import the following:
-
from django.shortcuts import render from rest_framework import status from rest_framework.response import Response from rest_framework.views import APIView from .models import Post from .serializers import PostSerializer
-
- create the
PostList
class, it should inherit fromAPIView
-
def
ine theget
method, which whould take 2 arguments:self
andrequest
-
inside the
get
request, create a variable calledposts
that retrievesall()
objects
fromPost
-
create the
serializer
variable that houses thePostSerializer
established inserializers.py
. pass it:- the
posts
variable - the
many
parameter, set toTrue
context
, which has a {K: V} Pair of K:'request'
V:request
- the
-
have the function
return
aresponse
, its parameter being the serializeddata
of theserializer
variablefrom django.shortcuts import render from rest_framework import status from rest_framework.response import Response from rest_framework.views import APIView from .models import Post from .serializers import PostSerializer class PostList(APIView): def get(self, request): posts = Post.objects.all() serializer = PostSerializer( posts, many=True, context={'request': request} ) return Response(serializer.data)
-
create a new
urls.py
file in theposts
app- import:
path
fromdjango.urls
views
fromposts
(views.py
from the posts app)
- create the
urlpatterns
list.- add a
path
for'posts/'
that takesPostlist
fromviews
as_view()
- add a
from django.urls import path from posts import views urlpatterns = [ path('posts/', views.PostList.as_view()), ]
- import:
-
in
drf_api/urls.py
include
theurls
fromposts
from django.contrib import admin from django.urls import path, include urlpatterns = [ path('admin/', admin.site.urls), path('api-auth/', include('rest_framework.urls')), path('', include('profiles.urls')), path('', include('posts.urls')), ]
-
run the server, check the get request works so far (add /posts/ to the browser window url)
To actually have posts appear, we have to make it possible for our users to create them. To achieve that, we have to define the post method inside the PostList view.
the post
method needs to achieve the following:
- deserialize request data
- if the data is valid, save the post with the user as the owner
- return the post with the 201 CREATED code
- if the data is invalid, return the ERROR: 400 BAD REQUEST code
steps:
- in the
PostList
class inposts/views.py
,def
ine thepost
method, passing it in the arguments ofself
andrequest
: - inside the
post
method, create theserializer
variable, its value being thePostSerializer
, which will in-turn, handle the following parameters:data
, which will have the value ofdata
from therequest
context
, which has a {K: V} Pair of K:'request'
V:request
- inside the function, below the
serializer
, write anif
statement that checks if theserializer
is_valid()
- if it is, make the
serializer
save()
the post, passing a parameter tosave()
that defines theowner
of the post to be theuser
that made therequest
- then, close the
if
statement byreturn
ing aResponse
that contains thedata
fromserializer
and astatus
that has a value ofHTTP_201_CREATED
fromstatus
codes
- if it is, make the
- below the
if
statement, in lieu of anelse
statement,return
aResponse
that takes anyerrors
raised byserializer
and pass astatus
ofHTTP_400_BAD_REQUEST
fromstatus
-
To have a nice create post form rendered in the preview window, let’s also set the serializer_class attribute to PostSerializer on our PostList class.
- at the top of the class, add a variable called
serializer_class
, its value should bePostSerializer
- at the top of the class, add a variable called
from django.shortcuts import render
from rest_framework import status
from rest_framework.response import Response
from rest_framework.views import APIView
from .models import Post
from .serializers import PostSerializer
class PostList(APIView):
serializer_class = PostSerializer
def get(self, request):
posts = Post.objects.all()
serializer = PostSerializer(
posts,
many=True,
context={'request': request}
)
return Response(serializer.data)
def post(self, request):
serializer = PostSerializer(
data=request.data, context={'request': request}
)
if serializer.is_valid():
serializer.save(owner=request.user)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
the Post List view has now been created, however, at this point, if an unauthenticated user tries to create a post, django will throw an error.
this can be mitigated using the permissions
framwork from rest
- add
permissions
as an import fromrest_framework
at the top ofviews.py
- at the top of the
PostList
class, below theserializer_classes
variable, add thepermission_classes
list variable, giving it a single entry in the list of:permissions.IsAuthenticatedOrReadOnly
from django.shortcuts import render
from rest_framework import status, permissions
from rest_framework.response import Response
from rest_framework.views import APIView
from .models import Post
from .serializers import PostSerializer
class PostList(APIView):
serializer_class = PostSerializer
permission_classes = [
permissions.IsAuthenticatedOrReadOnly
]
def get(self, request):
posts = Post.objects.all()
serializer = PostSerializer(
posts,
many=True,
context={'request': request}
)
return Response(serializer.data)
def post(self, request):
serializer = PostSerializer(
data=request.data, context={'request': request}
)
if serializer.is_valid():
serializer.save(owner=request.user)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
go back to the preview and check it all works.
this section covers:
- create the PostDetail view
- retrieve a record by id (get)
- update a record by id (put)
- delete a record by id (delete)
Post CRUD Table:
Let’s have a look at our CRUD table. It shows what kind of HTTP request our users have to make and to which URL in order to list all posts, create, read, update or delete a post.
HTTP | URI | CRUD Operation | View Name |
---|---|---|---|
a.GET / b.POST | /posts | a.list all posts / b.create a post | LIST |
c.GET / d.PUT / e.DELETE | /posts/:id | c.retrieve a post by id / d.update a post by id / e.delete a post by id | DETAIL |
steps:
- go to
posts/views.py
, start by importing the following:from django.Http import Http404
- the custom made permission
IsOwnerOrReadOnly
frompermissions.py
indrf_api
from drf_api.permissions import IsOwnerOrReadOnly
- create a new class called
PostDetail
that inherits fromAPIView
- add the
permission_classes
, there should only be one in the list for now, theIsOwnerOrReadOnly
permissionpermission_classes = [IsOwnerOrReadOnly]
- To format the edit form right out the gate, add the
serializer_class
too, which will bePostSerializer
serializer_class = PostSerializer
-
write the
get_object
method for the view, it should takeself
andpk
as arguments- add in a
try
statement which creates a variable calledpost
whichget
s anobject
fromPost
that has apk
that matches thepk
passed in as the argument for the method -post = Post.objects.get(pk=pk)
- then, check the permissions of the post and if the accessing user has permission to edit it by using
check_object_permissions
on the instance(this
), passing it the arguments ofself.request
and thepost
variableself.check_object_permissions(self.request, post)
return
thepost
variable- then, add an
except
ion for in the event that aPost
DoesNotExist
, in which case, theHttp404
error should beraise
d
- add in a
-
now, ceate the actual
get
method of the view, passing itself
,request
andpk
as arguments- create a variable called
post
that uses the just-definedget_object
method on the instance thats calling it (self
) and pass it an argument containing thepk
- beneath that, add a
serailizer
variable that takes thePostSerializer
and runs the newly-definedpost
variable through it as a parameter, in addition to acontext
parameter with a {Key: Value}Pair of'request': request
return
aResponse
containing thedata
from theserializer
from django.shortcuts import render from django.http import Http404 from rest_framework import status, permissions from rest_framework.response import Response from rest_framework.views import APIView from .models import Post from .serializers import PostSerializer from drf_api.permissions import IsOwnerOrReadOnly ... class PostDetail(APIView): permission_classes = [ IsOwnerOrReadOnly ] serializer_class = PostSerializer def get_object(self, pk): try: post = Post.objects.get(pk=pk) self.check_object_permissions(self.request, post) return post except Post.DoesNotExist: raise Http404 def get(self, request, pk): post = self.get_object(pk) serializer = PostSerializer( post, context={'request': request} ) return Response(serializer.data)
- create a variable called
-
now, add the path for the new view in
posts/urls.py
:- in
urlpatterns
add a newpath
forposts/<int:pk>
, where thePostDetail
from views is renderedas_
aview()
from django.urls import path from posts import views urlpatterns = [ path('posts/', views.PostList.as_view()), path('posts/<int:pk>/', views.PostDetail.as_view()), ]
- in
more detailed explanation on how to do this in the profile, although be sure to add context
to the serializer
as it isn't done in that part of the tutorial yet
-
def put(self, request, pk): post = self.get_object(pk) serializer = PostSerializer( post, data=request.data, context={'request': request} ) if serializer.is_valid(): serializer.save() return Response(serializer.data) return Response( serializer.errors, status=status.HTTP_400_BAD_REQUEST )
-
in the class,
def
ine adelete
method that takesself
,request
andpk
as parameters- define the
post
variable and give it a value using theget_object
method on the instance(this
) by using thepk
post = self.get_object(pk)
- run the
delete()
action on thepost
variablepost.delete()
return
aResponse
containing astatus
parameter with the value ofHTTP_204_NO_CONTENT
fromstatus
return Response(status=status.HTTP_204_NO_CONTENT)
def delete(self, request, pk): post = self.get_object(pk) post.delete() return Response( status=status.HTTP_204_NO_CONTENT )
- define the
-
check it all works
finished view code:
from django.shortcuts import render
from django.http import Http404
from rest_framework import status, permissions
from rest_framework.response import Response
from rest_framework.views import APIView
from .models import Post
from .serializers import PostSerializer
from drf_api.permissions import IsOwnerOrReadOnly
...
class PostDetail(APIView):
permission_classes = [
IsOwnerOrReadOnly
]
serializer_class = PostSerializer
def get_object(self, pk):
try:
post = Post.objects.get(pk=pk)
self.check_object_permissions(self.request, post)
return post
except Post.DoesNotExist:
raise Http404
def get(self, request, pk):
post = self.get_object(pk)
serializer = PostSerializer(
post,
context={'request': request}
)
return Response(serializer.data)
def put(self, request, pk):
post = self.get_object(pk)
serializer = PostSerializer(
post, data=request.data, context={'request': request}
)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
return Response(
serializer.errors, status=status.HTTP_400_BAD_REQUEST
)
def delete(self, request, pk):
post = self.get_object(pk)
post.delete()
return Response(
status=status.HTTP_204_NO_CONTENT
)
- run the command in the terminal to make a new app, name it
comments
python3 manage.py startapp comments
- add it to
INSTALLED_APPS
in settings.py
-
in
comments/models.py
import the following:models
fromdjango.db
User
fromdjango.contrib.auth.models
Post
fromposts.models
from django.db import models from django.contrib.auth.models import User from posts.models import Post
-
create a class called
Comment
, it should inherit frommodels.Model
-
the new
Comment
class needs the following fields:- owner, which should be a
ForeignKey
using theUser
as its value, it should alsoCASCADE
delete any related sub-items if deleted - post, which should be a
ForeignKey
using thePost
as its value, it should alsoCASCADE
delete any related sub-items if deleted - created_at, which should be a
DateTimeField
which should be automatically assigned a new value upon record creation by using the parameterauto_now_add=True
- updated_at, same as above, except its paramater is
auto_now=True
, notauto_add_now
as it updates every time the post is edited, not when it isadd
ed - content, should be a
TextField
to house post content
- owner, which should be a
-
add a
Meta
class that orders posts by theDateTime
they werecreated_at
, with the most recent entry being first -
define a dunder
str
method that returns thecontent
ofself
the post model should look like this:
from django.db import models
from django.contrib.auth.models import User
from posts.models import Post
class Comment(models.Model):
"""
Comment model, related to User and Post
"""
owner = models.ForeignKey(User, on_delete=models.CASCADE)
post = models.ForeignKey(Post, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
content = models.TextField()
class Meta:
ordering = ['-created_at']
def __str__(self):
return self.content
- Migrate the completed model into the database
python3 manage.py makemigrations
python3 manage.py migrate
- create a new file in
comments
calledserializers.py
- inside the new
serailizers
import:serializers
fromrest_framework
- the
Comment
model from.models
- create a class called
CommentSerializer
which inherits fromserializers.ModelSerializer
- establish the following serailizer fields:
- owner: a
ReadOnlyField
that'ssource
is theusername
of theowner
specified in theComment
model - profile_id: a
ReadOnlyField
that'ssource
is theid
of theowner
specified in theComment
model (id
's are automatically created by django) - profile_image: a
ReadOnlyField
that'ssource
is theurl
of theimage
field beonging to theowner
, specified in theComment
model - is_owner: a variable which houses the
SerializerMethodField
for theserializer
- owner: a
- use the
is_owner
field to create a method (prefix it withget_
) with the following parameters:self
andobj
for object in question- the function should then take a
context
ualrequest
from whatever calls it (this
), and then checks if theuser
that made therequest
matches theobj
'sowner
,return
ing a boolean value depending on the outcome
- the function should then take a
- add a
Meta
class that:- determines the
model
the serializer its basing its structure from, n this case it isComment
- determine the
fields
it serializes and displays in a list variable as strings, they should be:- id
- owner
- profile_id
- profile_image
- post
- created_at
- updated_at
- content
- is_owner
- determines the
finished serializer shuold look like this:
from rest_framework import serializers
from .models import Comment
class CommentSerializer(serializers.ModelSerializer):
owner = serializers.ReadOnlyField(source='owner.username')
profile_id = serializers.ReadOnlyField(source='owner.id')
profile_image = serializers.ReadOnlyField(source='owner.image.url')
is_owner = serializers.SerializerMethodField()
# this variable above is used to house
# the requisite serializer it is called
# as a function below by prefixing the variable's
# name with 'get_'
def get_is_owner(self, obj):
"""
passes the request of a user into the serializer
from views.py
to check if the user is the owner of a record
"""
request = self.context['request']
return request.user == obj.owner
class Meta:
model = Comment
fields = [
'id',
'owner',
'profile_id',
'profile_image',
'created_at',
'updated_at',
'post',
'content',
'is_owner',
]
- in
comments/serializers.py
, create a new class calledCommentDetailSerializer
. make it inherit from the previously createdCommentSerializer
class - give it a variable of
post
: aReadOnlyField
that'ssource
is theid
of thepost
specified in theComment
model
class CommentDetailSerializer(CommentSerializer):
post = serializers.ReadOnlyField(source='post.id')
Because CommentDetailSerializer inherits from CommentSerializer, all its methods and attributes are already included e.g. Meta.
how to write CommentList
and Detail
views using generics.
The Django Documentation states that generic views were developed as a shortcut for common usage patterns. What this means is that we can achieve all the same functionality of the get, post, put and other class based view methods without having to repeat ourselves so much.
steps:
- go to
views.py
in thecomments
app. - import the following:
generics
fromrest_framework
IsOwnerOrReadOnly
frompermissions
indrf_api
Comment
from.models
CommentSerializer
andCommentDetailSerializer
from.serializers
- create a class called
CommentList
that inherits fromgenerics.ListCreateAPIView
-
As we want to both [list] and [create] comments in the ListView, instead of explicitly defining the post and get methods like we did before, I’ll extend generics’ [List][Create]APIView. Extending the [List]APIView means we won’t have to write the get method and the [Create]APIView takes care of the post method.
- list covers the get request
- create covers the post request
-
- set the
serailizer_class
toCommentSerializer
- set the
permission_classes
to[permissions.IsAuthenticatedOrReadOnly]
- Add a queryset variable that
get
sall()
theobjects
in theComment
table-
Instead of specifying only the model we’d like to use, in DRF we set the queryset attribute. This way, it is possible to filter out some of the model instances. This would make sense if we were dealing with user sensitive data like orders or payments where we would need to make sure users can access and query only their own data. In this case however, we want all the comments.
- Learn more about attribute and data filtering here
-
code so far:
```py
from rest_framework import generics, permissions
from drf_api.permissions import IsOwnerOrReadOnly
from .models import Comment
from .serializers import CommentSerializer, CommentDetailSerializer
class CommentList(generics.ListCreateAPIView): # step 3
serializer_class = CommentSerializer # step 4
permission_classes = [permissions.IsAuthenticatedOrReadOnly] # step 5
queryset = Comment.objects.all() # step 6
```
Before we test the view, we’ll have to make sure comments are associated with a user upon creation. We do this with generics by defining the perform_create method
-
def
ine a method calledperform_create
which takes the arguments ofself
andserializer
-
the
perform_create
method takes inself
andserializer
as arguments. Inside, we pass in the user making the request as owner into the serializer’s save method, just like we did in the regular class based views.
-
-
call the
save()
command on theserializer
, in the argument ofsave
define theowner
as theuser
making therequest
- views using generics:
from rest_framework import generics, permissions from drf_api.permissions import IsOwnerOrReadOnly from .models import Comment from .serializers import CommentSerializer, CommentDetailSerializer class CommentList(generics.ListCreateAPIView): # step 3 serializer_class = CommentSerializer # step 4 permission_classes = [permissions.IsAuthenticatedOrReadOnly] # step 5 queryset = Comment.objects.all() # step 6 def perform_create(self, serializer): # step 7 serializer.save(owner=self.request.user) #step 8
- views manually written equivalent:
class PostList(APIView): serializer_class = PostSerializer permission_classes = [ permissions.IsAuthenticatedOrReadOnly ] def get(self, request): posts = Post.objects.all() serializer = PostSerializer( posts, many=True, context={'request': request} ) return Response(serializer.data) def post(self, request): serializer = PostSerializer( data=request.data, context={'request': request} ) if serializer.is_valid(): serializer.save(owner=request.user) return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
More good news is that with generics, the request is a part of the context object by default. What this means is that we no longer have to pass it manually, like we did in the regular class based views.
- views using generics:
-
now, create the
urls
for the comments. create theurls.py
file in thecomments app
-
import the following into
comments/urls.py
:from django.urls import path
from comments import views
-
next, create the
urlpatterns
list beneath the imports, it should contain the following paths:'comments/'
which should take theCommentList
view
and render itas_view()
urlpatterns = [
path('comments/', views.CommentList.as_view()),
]
- then, in
drf_api/urls
add the'comments.urls'
to apath
by using theinclude
methodfrom django.contrib import admin from django.urls import path, include urlpatterns = [ path('admin/', admin.site.urls), path('api-auth/', include('rest_framework.urls')), path('', include('profiles.urls')), path('', include('posts.urls')), path('', include('comments.urls')), # step 12 ]
- go back to
comments/views
and create a class calledCommentDetail
and have it inherit fromgenerics.RetrieveUpdateDestroyAPIView
- from generics, the following methods are pulled in the inherited model name to automatically streamline the coding for the following types of requests:
Retrieve
dataUpdate
dataDestroy
data- jump back up here for a fuller explanataion
- documentation on generics
- from generics, the following methods are pulled in the inherited model name to automatically streamline the coding for the following types of requests:
- set the
permission_classes
list of the new class to the imported[IsOwnerOrReadOnly]
permission - set the
serializer_class
for the class toCommentDetailSerializer
-
Our serializer still needs to access the request, but as mentioned before, we don’t really need to do anything, as the request is passed in as part of the context object by default.
-
- define the
queryset
for the class, queryingall()
theobjects
in theComment
table
class CommentDetail(generics.RetrieveUpdateDestroyAPIView): # step 13
permission_classes = [IsOwnerOrReadOnly] # step 14
serializer_class = CommentDetailSerializer # step 15
queryset = Comment.objects.all() # step 16
- with the class created, head back to
comments/urls.py
and add a new path to theurlpatterns
list, this time it should be forcomments/<int:pk>/
, (int:pk: the primary key is being told to be handled as an integeter). It should render theCommentDetail
view
as_view()
urlpatterns = [
path('comments/', views.CommentList.as_view()),
path('comments/<int:pk>', views.CommentDetail.as_view()), # step 17
]
- Creating a new App
- Creating the Like model
- creating the Like serializer
- run the command in the terminal to make a new app, name it
likes
python3 manage.py startapp likes
- add it to
INSTALLED_APPS
in settings.py
- go to
likes/models.py
- add all of the following imports:
from django.db import models from django.db.models import UniqueConstraint from django.contrib.auth.models import User from posts.models import Post from comments.models import Comment
- create the
Like
class, that inherits frommodels.model
- add the following fields to the new class to create the data model:
- owner: should be a ForeignKey linked to the
User
table, with anon_delete
property of CASCADE - post: should be a ForeignKey with a
related_name
oflikes
, with anon_delete
property of CASCADE - created_at: a DateTimeField which has a paroperty of
auto_now_add=True
- owner: should be a ForeignKey linked to the
- assign the
Like
class aMeta
class which:- reverse orders items in the table based on the time they were created
add aunique_together
field between owner and postunique_together
will be deprecated in the future, django docs advise the use ofconstraints = [UniqueConstraint()]
instead- add a constraints variable that takes a list value containing a
UniqueConstraint
that links theowner
andpost
to each individual like, making them unique
With things like UniqueConstraint, it's not actually making particular changes to the model itself. The code you've written will check the request before it tries to alter the database....so in this case, check if the owner and post ID's are already present together. Metaclasses are used to provide things like validation, without needing to alter the actual model itself. Generally, it'll be things like setting the name, ordering, unique constraints etc.
- add a dunder
str
method to the end of the class thatreturn
s a string containing theowner
andpost
fields
- create a new file in
likes
calledserializers.py
- inside the new
serailizers
import:serializers
fromrest_framework
- the
Like
model from.models
- create a class called
LikeSerializer
which inherits fromserializers.ModelSerializer
- establish the following serailizer fields:
- owner: a
ReadOnlyField
that'ssource
is theusername
of theowner
specified in thePost
model
- owner: a
- add a
Meta
class that:- determines the
model
the serializer its basing its structure from, in this case it isLike
- determines the
fields
it serializes and displays in a list variable as strings, they should be:- id
- owner
- created_at
- post
- determines the
In this video, we will learn how to prevent our users from liking the same post twice.
Because the Like
model has a Meta
class that prevents duplicate likes from a single user on a single post, trying like a post more than once will cause an Integrity Error. to avoid this, update the serializer with a method that can handle the error in an exception using a try
/except
statement.
-
first, navigate to
serializers.py
in thelikes
app. at the top of the file, import:from django.db import IntegrityError
-
then, Inside the
LikeSerializer
class, after itsMeta
class:def
ine a newcreate
method that takes the arguments ofself
andvalidated_data
-
Handling duplicates with the rest framework is pretty easy. All we have to do is define the create method inside our LikeSerializer to return a complete object instance based on the validated data.
-
add a
try
statement that tries toreturn
the following:super().create(validated_data)
-
This create method is on the model serializer and for that reason I had to call “super()”.
-
-
then add an
except
ion on anIntegrityError
that:raise
s aValidationError
fromserializers
, for its paramters, add a {K: V}P of'detail'
:'possible duplicate'
from django.db import IntegrityError
from rest_framework import serializers
from .models import Like
class LikeSerializer(serializers.ModelSerializer):
owner = serializers.ReadOnlyField(source='owner.username')
class Meta:
model = Like
fields = [
'id',
'owner',
'created_at',
'post',
]
def create(self, validated_data):
try:
return super().create(validated_data)
except IntegrityError:
raise serializers.ValidationError({
'detail': 'possible duplicate'
})
In this video, we will get more practice with generics by creating LikeList and LikeDetail generic views.
steps:
- go to
views.py
in thelikes
app. - import the following:
generics
fromrest_framework
IsOwnerOrReadOnly
frompermissions
indrf_api
Like
from.models
LikeSerializer
from.serializers
- create a class called
LikeList
that inherits fromgenerics.ListCreateAPIView
-
As we want to both [list] and [create] comments in the ListView, instead of explicitly defining the post and get methods like we did before, I’ll extend generics’ [List][Create]APIView. Extending the [List]APIView means we won’t have to write the get method and the [Create]APIView takes care of the post method.
- list covers the get request
- create covers the post request
-
- set the
serailizer_class
toLikeSerializer
- set the
permission_classes
to[permissions.IsAuthenticatedOrReadOnly]
- Add a queryset variable that
get
sall()
theobjects
in theLike
table-
Instead of specifying only the model we’d like to use, in DRF we set the queryset attribute. This way, it is possible to filter out some of the model instances. This would make sense if we were dealing with user sensitive data like orders or payments where we would need to make sure users can access and query only their own data. In this case however, we want all the likes.
- Learn more about attribute and data filtering here
-
code so far:
```py
from django.shortcuts import render
from rest_framework import generics, permissions
from drf_api.permissions import IsOwnerOrReadOnly
from .models import Like
from .serializers import LikeSerializer
class LikeList(generics.ListCreateAPIView):
serializer_class = LikeSerializer
permission_classes = [permissions.IsAuthenticatedOrReadOnly]
queryset = Like.objects.all()
```
Before we test the view, we’ll have to make sure likes are associated with a user upon creation. We do this with generics by defining the perform_create method
-
def
ine a method calledperform_create
which takes the arguments ofself
andserializer
-
the
perform_create
method takes inself
andserializer
as arguments. Inside, we pass in the user making the request as owner into the serializer’s save method, just like we did in the regular class based views.
-
-
call the
save()
command on theserializer
, in the argument ofsave
define theowner
as theuser
making therequest
-
views using generics:
from django.shortcuts import render from rest_framework import generics, permissions from drf_api.permissions import IsOwnerOrReadOnly from .models import Like from .serializers import LikeSerializer class LikeList(generics.ListCreateAPIView): serializer_class = LikeSerializer permission_classes = [permissions.IsAuthenticatedOrReadOnly] queryset = Like.objects.all() def perform_create(self, serializer): serializer.save(owner=self.request.user)
-
views manually written equivalent:
class PostList(APIView): serializer_class = PostSerializer permission_classes = [ permissions.IsAuthenticatedOrReadOnly ] def get(self, request): posts = Post.objects.all() serializer = PostSerializer( posts, many=True, context={'request': request} ) return Response(serializer.data) def post(self, request): serializer = PostSerializer( data=request.data, context={'request': request} ) if serializer.is_valid(): serializer.save(owner=request.user) return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
More good news is that with generics, the request is a part of the context object by default. What this means is that we no longer have to pass it manually, like we did in the regular class based views.
-
-
now, create the
urls
for the comments. create theurls.py
file in thelikes
app -
import the following into
likes/urls.py
:from django.urls import path
from likes import views
-
next, create the
urlpatterns
list beneath the imports, it should contain the following paths:'likes/'
which should take theLikeList
view
and render itas_view()
urlpatterns = [
path('likes/', views.LikeList.as_view()),
]
- then, in
drf_api/views
add the'likes.urls'
to apath
by using theinclude
methodfrom django.contrib import admin from django.urls import path, include urlpatterns = [ path('admin/', admin.site.urls), path('api-auth/', include('rest_framework.urls')), path('', include('profiles.urls')), path('', include('posts.urls')), path('', include('comments.urls')), # step 12 path('', include('likes.urls')), # step 12 ]
- in
likes/views.py
create a class calledLikeDetail
and have it inherit fromgenerics.RetrieveDestroyAPIView
- from generics, the following methods are pulled in the inherited model name to automatically streamline the coding for the following types of requests:
Retrieve
dataDestroy
data-
There’s no need to implement like updates, as our users will create a like when clicking the like button and destroy a like when clicking the button again.
- jump back up here for a fuller explanataion
- documentation on generics
- from generics, the following methods are pulled in the inherited model name to automatically streamline the coding for the following types of requests:
- set the
permission_classes
list of the new class to the imported[IsOwnerOrReadOnly]
permission-
which will allow only the user who liked a post to un-like it
- review it here
-
- set the
serializer_class
for the class toLikeSerializer
-
Our serializer still needs to access the request, but as mentioned before, we don’t really need to do anything, as the request is passed in as part of the context object by default.
-
- define the
queryset
for the class, queryingall()
theobjects
in theLike
table
class LikeDetail(generics.RetrieveDestroyAPIView): # step 13
permission_classes = [IsOwnerOrReadOnly] # step 14
serializer_class = LikeSerializer # step 15
queryset = Like.objects.all() # step 16
- with the class created, head back to
likes/urls.py
and add a new path to theurlpatterns
list, this time it should be forlikes/<int:pk>/
, (int:pk: the primary key is being told to be handled as an integeter). It should render theLikeDetail
view
as_view()
urlpatterns = [
path('likes/', views.LiketList.as_view()),
path('likes/<int:pk>/', views.LikeDetail.as_view()), # step 17
]
-
create the app using the same methods used to create the other apps
-
create the
Follower
model with the following spec:
Product Spec
- owner: ForeignKey
- set on_delete to cascade
- set related_name to 'following'
- followed: ForeignKey
- set on_delete to cascade
- set related_name to 'followed'
- created_at: DateTimeField
- add auto_now_add as True
Additionally:
- Create the Meta class:
- add ordering field using reverse created_at.
- Add unique_together field between owner and followed
- Create the str dunder method:
- return a string containing the owner and followed fields
from django.db import models
from django.db.models import UniqueConstraint
from django.contrib.auth.models import User
from profiles.models import Profile
# Create your models here.
class Follower(models.Model):
owner = models.ForeignKey(User, on_delete=models.CASCADE, related_name='following')
followed = models.ForeignKey(User, on_delete=models.CASCADE, related_name='followed')
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ['-created_at']
constraints = [UniqueConstraint(fields=['owner', 'followed'], name='unique_follow')]
def __str__(self):
return f"{self.owner} {self.followed}"
challenge: https://learn.codeinstitute.net/courses/course-v1:CodeInstitute+DRF+2021_T1/courseware/601b5665c57540519a2335bfbcb46d93/d29d4cc768c944b1b6127f429bc14c97/?child=first
all steps can be completed using previous lessons
Project Description In the last challenge, we created our Follower Model. In this section, you will create the FollowerSerializer.
Important: Please ensure that your models are correct, and that you have migrated your models to the database. Use the following Followers Model Code to check, if you haven't done so already.
- Serializer Spec Now, your challenge is to create the FollowerSerializer, here’s the spec:
Product Spec
- owner: read only field
- followed_name: read only field
Additionally:
- Add the Meta class
- Add a 'create' function to handle duplication errors
-
Steps Important: As previously mentioned, it is highly recommended to refer to a previous example of a serializer, as there will be less helpful hints in this challenge than the previous 'like serializer' challenge
-
'followers' app Create the followers/serializers.py file. Add the relevant imports (x3).
-
FollowerSerializer class Use the spec document above to create the FollowerSerializer class including the owner, and followed_name fields, and meta class. Add a create function to handle Integrity Errors.
from django.db import IntegrityError
from rest_framework import serializers
from .models import Follower
class FollowerSerializer(serializers.ModelSerializer):
owner = serializers.ReadOnlyField(source='owner.username')
followed_name = serializers.ReadOnlyField(source='followed.username')
class Meta:
model = Follower
fields = [
'id',
'owner',
'followed_name',
'followed',
'created_at',
]
def create(self, validated_data):
try:
return super().create(validated_data)
except IntegrityError:
raise serializers.ValidationError({
'detail': 'possible duplicate'
})
solution code: https://github.com/Code-Institute-Solutions/drf-api/tree/5c6997afa15b3fc83bbfb5e7521fdb5711a021e5
Generic Views Spec Now, your challenge is to create the FollowerList generic view, here’s the spec:
Product Spec
- FollowerList: call upon the subclass to GET and POST
- set the serializer_class to FollowerSerializer
- set the queryset to all followers
- set the permission class to IsAuthenticateOrReadOnly
Additionally:
- Add the perform_create function.
- FollowerDetail: call upon the subclass to RETRIEVE and DELETE
- set the serializer_class to FollowerSerializer
- set the queryset to all followers
- set the permission class to IsOwnerOrReadOnly
Steps Important: As previously mentioned, it is highly recommended to refer to a previous example of a generics view, such as 'likes / views.py'.
-
FollowerList
- Add the appropriate imports (x4).
- Pass the appropriate generics views into your class so that you can GET and POST your views.
- Create the appropriate attributes based on the Product Spec above
- Add the perform_create function (refer to likes/views.py)
-
FollowerDetail class
- Pass the appropriate generics views into your class so that you can Retrieve and Delete your views.
- Create the appropriate attributes based on the Product Spec above
-
URLs
- Add your urls to the appropriate files.
from django.shortcuts import render
from rest_framework import generics, permissions
from drf_api.permissions import IsOwnerOrReadOnly
from .models import Follower
from .serializers import FollowerSerializer
class FollowerList(generics.ListCreateAPIView):
serializer_class = FollowerSerializer
permission_classes = [permissions.IsAuthenticatedOrReadOnly]
queryset = Follower.objects.all()
def perform_create(self, serializer):
serializer.save(owner=self.request.user)
class FollowerDetail(generics.RetrieveDestroyAPIView):
serializer_class = FollowerSerializer
permission_classes = [IsOwnerOrReadOnly]
queryset = Follower.objects.all()
- refactor the profiles and posts views to use generics
- all the information in the previous sections should be sufficient to complete this task
if yo get stuck, here are the solution links:
- profiles:
https://github.com/Code-Institute-Solutions/drf-api/blob/23b93337ab45903140ea01232474e9fbcad4f015/profiles/views.py
- posts:
https://github.com/Code-Institute-Solutions/drf-api/blob/23b93337ab45903140ea01232474e9fbcad4f015/posts/views.py
- mind that the posts views still requires a
perform_create
function to bind the posting User's primary Key to the new post
- adding an extra field to the profile serializer to include whether or not a logged in user has followed another user.
- basically, whenever a user follows another user, the Follower table updates with a record with a value of
- the owner (the person who made the follow)
- and the followed (the profile the user followed.)
any time a follow is made by any user, a new record is created with these values. These values are regulated as unique by the serializer from the previous lessons.
this lesson updates the
ProfileSerializer
to - when an authenticated user logs in - filter through theFollower
table and return records that are owned by that user, as this is an iterative process, it will do it for every record in the Table, meaning that is a list is generated, it will return a value for each record. In this instance, the serializer will return the id of the follow. the serializer will also use anif
statement to check that a user is authenticated, if they are not, the filter for follows will not run, restulting in returning anull
value for each record.
- basically, whenever a user follows another user, the Follower table updates with a record with a value of
- go to
profiles/serializers.py/ProfileSerializer
and import theFollower
modelfrom followers.models import Follower
- then, inside the
ProfileSerializer
add a new variable calledfollowing_id
. it should have the same value asis_owner
, the variable above it, so that it creates a new field in the serializer. def
ine a new method calledget_following_id
, which calls theget_
prefix on the new SerializerMethodField just created. (more about theget_
prefix here).- pass it arguments of
self
andobj
- pass it arguments of
- inside the new method, create a variable called
user
, its value should be theuser
from the['request']
of thecontext
of theself
argument that had been passed in as an argument.user = self.context['request'].user
-
Inside, I’ll get the current user from the context object and check if the user is authenticated.
- add a conditional stateent to the function that,
if
theuser
.is_authenticated
:-
Then I will check if the logged in user is following any of the other profiles.
- create a variable called
following
, assign it the value of theobjects
of theFollower
tablefilter
ed by:owner
, where the value is the authenticateduser
followed
, where the value is theowner
of theobj
parameter passed in at the top of the function
- append the value with
.first()
so that it will only return the one value any time it runs.
-
- add a print statement into the function to check its working correctly
- then, at the end of the
if
statement,return
theid
of the newly establishedfollowing
variable from inside theif
statement, appending an additionalif
statement onto it in thereturn
statement to double check if following istruthy
(has a value), else the statement willreturn`` the value of
None`return following.id if following else None
- in lieu of an
else
statement on the outerif
statement, provide areturn
statement that returnsNone
in the event that the user is not authenticated
Finished code:
from rest_framework import serializers
from .models import Profile
from followers.models import Follower
class ProfileSerializer(serializers.ModelSerializer):
owner = serializers.ReadOnlyField(source='owner.username')
is_owner = serializers.SerializerMethodField()
following_id = serializers.SerializerMethodField()
def get_is_owner(self, obj):
"""
passes the request of a user into the serializer
from views.py
to check if the user is the owner of a record
"""
request = self.context['request']
return request.user == obj.owner
# new function
def get_following_id(self, obj):
user = self.context['request'].user
if user.is_authenticated:
following = Follower.objects.filter(
owner=user, followed=obj.owner
).first()
# print("FOLLOWING!!!!!!!!!!!!!!!!: ", following)
return following.id if following else None
return None
class Meta:
model = Profile
fields = [
'id',
'owner',
'created_at',
'updated_at',
'name',
'content',
'image',
'is_owner',
'following_id', # new field
]
as this theoretically works the exact same way is follows does with profiles, copy the steps above to achieve the same outcome for posts and likes:
ed result code following above method:
class PostSerializer(serializers.ModelSerializer):
owner = serializers.ReadOnlyField(source='owner.username')
profile_id = serializers.ReadOnlyField(source='owner.id')
profile_image = serializers.ReadOnlyField(source='owner.image.url')
is_owner = serializers.SerializerMethodField()
# this variable above is used to house
# the requisite serializer it is called
# as a function below by prefixing the variable's
# name with 'get_'
like_id = serializers.SerializerMethodField()
# newly added variable to run the serializer
# to get an id of a like associated with the post
# and the authenticated user
def get_is_owner(self, obj):
"""
passes the request of a user into the serializer
from views.py
to check if the user is the owner of a record
"""
request = self.context['request']
return request.user == obj.owner
def get_like_id(self, obj):
# the like_id variable is prefixed with get_ to run
# a specific method type
user = self.context['request'].user
# the user variable gets the user that
# prompted the method to run
if user.is_authenticated:
# checks if the user is authenticated
# if they are:
liked = Like.objects.filter(owner=user, post=obj).first()
# the liked variable runs on each entry in Post
# and checks is a user is the owner of a like for that post
print(liked)
# just a print statement to check its all kushty in the CLI
return liked.id if liked else None
# for each entry, the serializer will return the id of the like
# created for that post by that authenticated user
# if the user hasnt made one, the method will return a "None" value
return None
# skip to end if user isnt authenticated and return "None" for all Posts
def validate_image(self, value):
if value.size > 1024 * 1024 * 2:
raise serializers.ValidationError(
'Image size larger than 2MB!'
)
if value.image.width > 4096:
raise serializers.ValidationError(
'Image wider than 4096 pixels!'
)
if value.image.height > 4096:
raise serializers.ValidationError(
'Image taller than 4096 pixels!'
)
return value
class Meta:
model = Post
fields = [
'id',
'owner',
'profile_id',
'profile_image',
'created_at',
'updated_at',
'title',
'content',
'image',
'image_filter',
'is_owner',
'like_id', # and don't forget to add the field!
]
using the ManyToMany field to link multiple entries in one table to a single entry in another
- create an app called publications that contains the following fields:
- title (charfield)
- completed model should look like this:
from django.db import models
# Create your models here.
class Publication(models.Model):
title = models.CharField(max_length=250)
class Meta:
ordering = ['title']
def __str__(self):
return f"publication {self.pk}: {self.title}"
- make sure to add it to
INSTALLED_APPS
in settings.py - also create list and detail views using generics
- create a serializer to serialize the data into JSON
- register the model in the apps admin.py
- link everything up in a urls.py in the app and then include those urls in the drf_api app
- repeat the above process for an articles app. here is the model:
from django.db import models
from publications.models import Publication
# Create your models here.
class Article(models.Model):
headline = models.CharField(max_length=250)
publications = models.ManyToManyField(Publication)
class Meta:
ordering = ['headline']
def __str__(self):
return f"{self.headline}"
test it all works. in the rpeview go to the admin panel and see that under the articles model you can select multiple publications to be associated with that article
- adding a number of posts a user has made to their profile
- adding a count of the followers a user has
- adding a count for the number of profiles a user is following
- using the annotate function
- make fields sortable with filters
- got to
views.py
in theprofiles
app
-
make a new import at the top of the file:
from django.db.models import Count
-
add
filters
to imports fromrest_framework
from rest_framework import generics, filters
-
in the
ProfileList
class, amend thequeryset
variable - instead of using.all()
on theProfile.objects
, use.annotate()
function-
The annotate function allows us to define extra fields to be added to the queryset. In our case, we’ll add fields to work out how many posts and followers a user has, and how many other users they’re following.
- inside the
annotate
function, pass in the following:posts_count
which will be equal to theCount()
class imported in step 2-
what we’re trying to achieve here is to count the number of posts associated with a specific Profile.
however, there is no direct relationship between Profile and Post. So we need to go through the User model to get there.
So, inside the Count class, we will need to perform a lookup that spans the profile, user, and post models, so we can get to the Post model with the instances we want to count.
-
posts_count=Count('owner__post', distinct=True)
Similar to when we used dot notation, the first part of our lookup string is the owner field on the Profile model, which is a OneToOne field referencing User. From there we can reach the Post model. So we have to add ‘double underscore post’ to show the relationship between Profile, User and Post.
we also need to pass distinct=True here to only count the unique posts. Without this we would get duplicates.
-
class ProfileList(generics.ListAPIView): """ List all profiles. No create view as profile creation is handled by django signals. """ serializer_class = ProfileSerializer queryset = Profile.objects.annotate( posts_count=Count('owner__post', distinct=True), )
-
-
inside the queryset, beneath the
posts_count
parameter, add afollowers_count
parameter that also uses theCount()
method-
This time we have a problem. Within the Follower model, we have two foreign keys that are referencing the User model. One to identify the User following another user and the other to identify the one being followed.
So here, we need to use the related_names “following” and “followed” defined in followers’ models.py file, instead of the model name like we did for owner__post. The string value then, will be ‘owner__followed’.
-
inside the
Count()
method offollowers_count
, use the double underscore notation (__
) on owner to connect to thefollowed
field of theFollower
model by its related name offollowed
(this may be the same as the variable name, but remember, this is the value its using to connect the fields, NOT the variable name)followers_count=Count('owner__followed', distinct=True),
class ProfileList(generics.ListAPIView): """ List all profiles. No create view as profile creation is handled by django signals. """ serializer_class = ProfileSerializer queryset = Profile.objects.annotate( posts_count=Count('owner__post', distinct=True), followers_count=Count('owner__followed', distinct=True), )
-
-
repeat step 5 for the to count the amount of profiles a user is
following
, it should work the exact same way asfollowers_count
as thefollowing
variable in theFollower
model has a related name offollowing
-
with all 3 paramters added,
order
thequeryset
valuesby
they date they werecreated_at
in reverse.class ProfileList(generics.ListAPIView): """ List all profiles. No create view as profile creation is handled by django signals. """ serializer_class = ProfileSerializer queryset = Profile.objects.annotate( posts_count=Count('owner__post', distinct=True), followers_count=Count('owner__followed', distinct=True), following_count=Count('owner__following', distinct=True), ).order_by('-created_at')
-
after the
queryset
variable, set a new variable calledfilter_backends
, which contains a list value with one entry offilters.OrderingFilter
(taken from thefilters
import)filter_backends = [filters.OrderingFilter]
-
now, after that, create another variable called
ordering_fields
which contains a list of the fields counted in theannotate
method of thequeryset
aboveordering_fields = ['posts_count', 'followed_count', 'following_count']
-
I’d also like to be able to sort our profiles by how recently they followed a profile and by how recently they have been followed by a profile.
- in the
ordering_fields
list, add theowner__followers
andowner__following
like in the count methods, except this time, append them withcreated_at
with__
-
class ProfileList(generics.ListAPIView): """ List all profiles. No create view as profile creation is handled by django signals. """ serializer_class = ProfileSerializer queryset = Profile.objects.annotate( posts_count=Count('owner__post', distinct=True), followers_count=Count('owner__followed', distinct=True), following_count=Count('owner__following', distinct=True), ).order_by('-created_at') ordering_fields = [ 'post_count', 'followed_count', 'following_count', 'owner__followed__created_at', 'owner__following__created_at' ]
As these are regular database fields, I don’t need to add them to the queryset, but I still have to add them to the ordering_fields list.
- in the
-
To make sure these new ordering/ordered fields can be viewed, they need to be passed into the serializer:
from rest_framework import serializers
from .models import Profile
from followers.models import Follower
class ProfileSerializer(serializers.ModelSerializer):
owner = serializers.ReadOnlyField(source='owner.username')
is_owner = serializers.SerializerMethodField()
following_id = serializers.SerializerMethodField()
posts_count = serializers.ReadOnlyField()
followers_count = serializers.ReadOnlyField()
following_count = serializers.ReadOnlyField()
def get_is_owner(self, obj):
"""
passes the request of a user into the serializer
from views.py
to check if the user is the owner of a record
"""
request = self.context['request']
return request.user == obj.owner
def get_following_id(self, obj):
user = self.context['request'].user
if user.is_authenticated:
following = Follower.objects.filter(
owner=user, followed=obj.owner
).first()
print(following)
return following.id if following else None
return None
class Meta:
model = Profile
fields = [
'id',
'owner',
'created_at',
'updated_at',
'name',
'content',
'image',
'is_owner',
'following_id',
'posts_count',
'followed_count',
'following_count',
]
If we look at a post retrieved from our API, we don’t have any information about the number of comments or likes the post has. As a challenge, I’d like to ask you to add two new fields to retrieved posts for:
- comments_count
- likes_count
All of these fields will need to be added to the queryset
- Product Spec Now, your challenge is to create the Post fields and Post filters, here’s the spec:
Creating your Post Fields
- In post/views.py, in the PostList class, adjust the queryset to include the following fields:
- comments_count
- likes_count
- Order the fields by created_at
- In posts/serializers.py:
- Define both comments_count and likes_count as ReadOnly fields in the PostSerializer
- Include them both in the fields list
Adding Fields to the Filter
- In post/views.py, within the PostList class, create the filter so that you can filter by
- comments_count
- likes_count
- likes__created_at
Adding fields to the PostDetail class
- Adjust the PostDetail queryset to include the following fields:
- comments_count
- likes_count
- Order the fields by created_at
steps:
Part 1: Creating your PostList Fields In posts/views.py:
- Add the required import.
- In the PostList class, adjust the queryset attribute:
- Use the annotate function.
- Define the comments_count field which should Count the number of comments which are distinct.
- Define the likes_count field which should Count the number of likes that are distinct.
- Order the annotated fields by how recent Posts are with the most recently created ones first. In posts/serializer.py
- Define both comments_count and likes_count as ReadOnly fields in the PostSerializer.
- Include them both in the fields list.
Part 2: Adding Fields to the Filter In posts/views.py:
- Add the required import.
- Create the filter_backends list, to use OrderingFilter.
- Add the ordering_fields list, to filter by the appropriate fields.
- Make sure to add a filter to allow you to sort posts by how recently they've been liked.
Part 3: Adding fields to the PostDetail class In posts/views.py: 2. In the PostDetail class, copy and paste the PostList queryset into the PostDetail class (removing the existing queryset code). 3. Your new queryset should contain 2 fields: comments_count, likes_count
- create text search for Posts, search either by Author's username or port title
- how to implement text search feature on a view
- inside
posts
'views.py
file, in thePostList
class:- add another filter to the
filter_backends
list calledfilters.SearchFilter
- now, create a new variable called
search_fields
that takes a list of values to search by. in this case it will beowner__username
to get the owner of a post andtitle
for the title of the post
- add another filter to the
- run the environment, see if it works. search field should be in the filter button
from django.db.models import Count
from django.shortcuts import render
from django.http import Http404
from rest_framework import status, permissions, generics, filters
from rest_framework.response import Response
from rest_framework.views import APIView
from .models import Post
from .serializers import PostSerializer
from drf_api.permissions import IsOwnerOrReadOnly
class PostList(generics.ListCreateAPIView):
"""
List posts or create a post if logged in
The perform_create method associates the post with the logged in user.
"""
serializer_class = PostSerializer
permission_classes = [
permissions.IsAuthenticatedOrReadOnly
]
queryset = Post.objects.annotate(
comments_count=Count('comment', distinct=True),
likes_count=Count('likes', distinct=True)
).order_by('created_at')
filter_backends = [
filters.OrderingFilter,
filters.SearchFilter # new filter added here
]
ordering_fields = [
'comments_count',
'likes_count',
'likes__created_at',
]
search_fields = [ # new variable for search fields added here
'owner__username',
'title',
]
def perform_create(self, serializer):
serializer.save(owner=self.request.user)
class PostDetail(generics.RetrieveUpdateDestroyAPIView):
"""
Retrieve a post and edit or delete it if you own it.
"""
serializer_class = PostSerializer
permission_classes = [IsOwnerOrReadOnly]
queryset = Post.objects.annotate(
comments_count=Count('comment', distinct=True),
likes_count=Count('likes', distinct=True)
).order_by('created_at')
filter_backends = [
filters.OrderingFilter
]
ordering_fields = [
'comments_count',
'likes_count',
'likes__created_at',
]
- add a filter feature to filter the API data
- posts owned by a user
- posts liked by a user
- posts by another user that a user us following
- filter user list of accounts a user is following
- using django_filters library
this section was a bit much trying to understand the traversal of tables, so rewatch the video from 1.52
to use filtering methods in this section, the project first needs to have the django_filters
library installed. install it via the CLI using the following command:
pip3 install django-filter
then, include it in INSTALLED_APPS
in settings.py
as django_filters
then, update requirements.txt: pip3 freeze > requirements.txt
to then use django_filters
, the following import needs to be made on any file wanting to use the library:
from django_filters.rest_framework import DjangoFilterBackend
in the views.py
of the posts
app:
-
import django filters
from django_filters.rest_framework import DjangoFilterBackend
-
inside the
PostList
view, addDjangoFilterBackend
to thefilter_backends
list variable -
create a variable called
filterset_fields
which is a list-
To get the user post feed by their profile id, we’ll have to take the following steps:
First, we'll have to find out who owns each post in the database. Next, we'll need to see if a post owner is being followed by a specific user. Finally, we'll need to point to that user's profile so that we can use its id to filter our results.
Similar to using dot notation from previous sections, we'll have to figure out how to navigate our tables.
-
- to filter posts by another user that a user if following, add the followingvalue to the
filerset_fields
list:- refenrence the
owner
of the post, which will give access to theUser
table, - this gives access to referencing the
Follower
table by using thefollowed
related_name
. theFollower
table can now be queried to see if any entries that have afollowed
value equal to thePost.owner
and if the person that owns thatfollowed
value is the accessing user (owner
, as inFollower.owner
), then, the accessingowner
(Follower.owner
)'s profile is called by usingprofile
'owner__followed__owner__profile'
Model instances can always be filtered by id, so we don’t need to add “double underscore id”.
- refenrence the
- as
Post
andLike
tables are linked by a related_name oflikes
,likes
can be used to filter to theowner
of the like, which is aForeignKey
linked to theUser
table, which is also linked to theProfile
table by its ownowner
field'likes__owner__profile'
-
as
Post
is linked toUser
via theowner
field, theProfile
table can be accessed directly with double underscore notation'owner__profile'
-
add these fields to
filterset_fields
should produce a result like this:
from django.db.models import Count
from django.shortcuts import render
from django.http import Http404
from django_filters.rest_framework import DjangoFilterBackend # new filter import here
from rest_framework import status, permissions, generics, filters
from rest_framework.response import Response
from rest_framework.views import APIView
from .models import Post
from .serializers import PostSerializer
from drf_api.permissions import IsOwnerOrReadOnly
class PostList(generics.ListCreateAPIView):
"""
List posts or create a post if logged in
The perform_create method associates the post with the logged in user.
"""
serializer_class = PostSerializer
permission_classes = [
permissions.IsAuthenticatedOrReadOnly
]
queryset = Post.objects.annotate(
comments_count=Count('comment', distinct=True),
likes_count=Count('likes', distinct=True)
).order_by('created_at')
filter_backends = [
filters.OrderingFilter,
filters.SearchFilter,
DjangoFilterBackend, # new filter added here
]
ordering_fields = [
'comments_count',
'likes_count',
'likes__created_at',
]
search_fields = [
'owner__username',
'title',
]
filterset_fields = [ # new fields added here
'owner__followed__owner__profile',
'likes__owner__profile',
'owner__profile',
]
def perform_create(self, serializer):
serializer.save(owner=self.request.user)
class PostDetail(generics.RetrieveUpdateDestroyAPIView):
"""
Retrieve a post and edit or delete it if you own it.
"""
serializer_class = PostSerializer
permission_classes = [IsOwnerOrReadOnly]
queryset = Post.objects.annotate(
comments_count=Count('comment', distinct=True),
likes_count=Count('likes', distinct=True)
).order_by('created_at')
filter_backends = [
filters.OrderingFilter
]
ordering_fields = [
'comments_count',
'likes_count',
'likes__created_at',
]
We'll be able to filter user profiles that follow a user with a given profile_id.
- go to profiles > views, import
from django_filters.rest_framework import DjangoFilterBackend
- add
DjangoFilterBackend
to thefilter_backends
list inProfileList
- create the
filterset_fields
list variable:- watch the walkthrough video from 6.16 to go through the explanation of this with diagrams
- add the following filter:
'owner__following__followed__profile'
updated code:
from django.db.models import Count
from django.http import Http404
from django.shortcuts import render
from django_filters.rest_framework import DjangoFilterBackend # new import added here
from rest_framework import status, generics, permissions, filters
from rest_framework.views import APIView
from rest_framework.response import Response
from .models import Profile
from .serializers import ProfileSerializer
from drf_api.permissions import IsOwnerOrReadOnly
class ProfileList(generics.ListAPIView):
"""
List all profiles.
No create view as profile creation is handled by django signals.
"""
serializer_class = ProfileSerializer
queryset = Profile.objects.annotate(
posts_count=Count('owner__post', distinct=True),
followers_count=Count('owner__followed', distinct=True),
following_count=Count('owner__following', distinct=True),
).order_by('-created_at')
filter_backends = [
filters.OrderingFilter,
DjangoFilterBackend, # new filter added here
]
ordering_fields = [
'post_count',
'followed_count',
'following_count',
'owner__followed__created_at',
'owner__following__created_at',
]
filterset_fields = [ # new field here
'owner__following__followed__profile',
]
class ProfileDetail(generics.RetrieveUpdateAPIView):
"""
Retrieve or update a profile if you're the owner.
"""
permission_classes = [IsOwnerOrReadOnly]
queryset = Profile.objects.all()
serializer_class = ProfileSerializer
- get a list of all of the accounts that follow a user
- in short, it is like getting the accounts a user follows in reverse:
'owner__following__followed__profile'
filters the accounts that a user follows'owner__followed__owner__profile
Project Description In this challenge, you'll be required to simply add the correct string to the filterset_fields within your profiles/views.py file.
You should first attempt to work out your answer by following the table below, before looking at the steps.
Check the Hints for the correct answer when you are finished.
- Product Spec Now, your challenge is to add the string in the filterset_fields list in order to:
get all profiles that are followed by a profile, given its id Use your existing filterset_fields list located within profiles/views.py
- Steps For example, in order to get all the profiles followed by Ronan, we’ll need to complete these steps:
Identify the profile owner, in this case let's use Adam, from the database of all profiles. Identify if that profile owner is being followed by our main user, Ronan. If it is, identify the "following" profile owner id. If this id is Ronan's, the "followed" profile, Adam, will be displayed.
This process will be repeated in order to filter all profiles in the database.
walkthrough:
- Use the 'owner' field to return to the User table
- Identify that 'Adam' is being followed (by anyone) using the 'followed' field.
- Identify who that follower is, in this case Ronan, using the 'owner' field.
- The 'owner' is a ForeignKey field which will return us to the User table.
- Return the profile id value by linking to 'profile', ('profile' will return the 'id' value automatically').
- This value is then used by drf to filter our profiles, e.g. if id = 1 (Ronan) then display Adam's profile.
from django.db.models import Count
from django.http import Http404
from django.shortcuts import render
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import status, generics, permissions, filters
from rest_framework.views import APIView
from rest_framework.response import Response
from .models import Profile
from .serializers import ProfileSerializer
from drf_api.permissions import IsOwnerOrReadOnly
class ProfileList(generics.ListAPIView):
"""
List all profiles.
No create view as profile creation is handled by django signals.
"""
serializer_class = ProfileSerializer
queryset = Profile.objects.annotate(
posts_count=Count('owner__post', distinct=True),
followers_count=Count('owner__followed', distinct=True),
following_count=Count('owner__following', distinct=True),
).order_by('-created_at')
filter_backends = [
filters.OrderingFilter,
DjangoFilterBackend,
]
ordering_fields = [
'post_count',
'followed_count',
'following_count',
'owner__followed__created_at',
'owner__following__created_at',
]
filterset_fields = [
'owner__following__followed__profile',
'owner__followed__owner__profile', # solution code here
]
class ProfileDetail(generics.RetrieveUpdateAPIView):
"""
Retrieve or update a profile if you're the owner.
"""
permission_classes = [IsOwnerOrReadOnly]
queryset = Profile.objects.all()
serializer_class = ProfileSerializer
- be able to retrieve all the comments associated with a given post.
Project Description In this challenge, you'll need to repeat all the steps required to create a filter in Comments.
Check the Hints for the correct answer when you are finished.
- Product Spec Now, your challenge is to create a filterset_field filter in Comments, in order to:
be able to retrieve all the comments associated with a given post.
- Steps Follow the steps to complete the tasks:
In Comments/views.py:
- Import the correct filter type. In this case, DjangoFilterBackend.
- Set the filter_backends attribute in the CommentsList view.
- Set the filterset_fields attribute to a list containing one item to filter as stated above. (Hint: start off with an example, e.g. to return all comments for Post 1)
because the Comment
and Post
tables are directly linked via foreign key, the filterset field can just be set to 'post'
from django.shortcuts import render
from django_filters.rest_framework import DjangoFilterBackend # new import here
from rest_framework import generics, permissions, filters # filters imported here
from drf_api.permissions import IsOwnerOrReadOnly
from .models import Comment
from .serializers import CommentSerializer, CommentDetailSerializer
class CommentList(generics.ListCreateAPIView):
serializer_class = CommentSerializer
permission_classes = [permissions.IsAuthenticatedOrReadOnly]
queryset = Comment.objects.all()
filter_backends = [
DjangoFilterBackend, # new filter added here
]
filterset_fields = [ # new field here
'post',
]
def perform_create(self, serializer):
serializer.save(owner=self.request.user)
class CommentDetail(generics.RetrieveUpdateDestroyAPIView):
permission_classes = [IsOwnerOrReadOnly]
serializer_class = CommentDetailSerializer
queryset = Comment.objects.all()
- using the django rest auth library
- adding authentication to the project
-
install the django rest auth package:
- Since this video was created, Django REST Auth has introduced a new version that will be automatically installed if you use the command in the video. To ensure that you get the version of dj-rest-auth that will work while following these videos, instead of the command pip3 install dj-rest-auth, please use this:
pip3 install dj-rest-auth==2.1.9
-
add
rest_framework.authtoken
anddj_rest_auth
toINTALLED_APPS
insettings.py
-
add
dj-rest-auth/
in the main appurls.py
, add aninclude
to it that passes in'dj_rest_auth.urls'
from django.contrib import admin from django.urls import path, include urlpatterns = [ path('admin/', admin.site.urls), path('api-auth/', include('rest_framework.urls')), path('dj-rest-auth/', include('dj_rest_auth.urls')), path('', include('profiles.urls')), path('', include('posts.urls')), path('', include('comments.urls')), path('', include('likes.urls')), path('', include('followers.urls')), ]
-
as new apps have been installed, migrate the database
-
install Django allAuth with the following install command:
pip3 install 'dj-rest-auth[with_social]'
-
Once that's installed, add these new apps to the
INSTALLED_APPS
list insettings.py
-
'django.contrib.sites', 'allauth', 'allauth.account', 'allauth.socialaccount', 'dj_rest_auth.registration',
-
-
below
INSTALLED_APPS
insettings.py
, as a seperate variable calledSITE_ID
and give it the value of1
SITE_ID = 1
-
in the main
urls.py
, add the following path:path('dj-rest-auth/registration/', include('dj_rest_auth.registration.urls'))
Because DRF doesn’t support JWT tokens for the browser interface out-of-the-box, we’ll need to use session authentication in development. And for Production we’ll use Tokens. This will allow us to continue to be able to log into our API as we work on it.
- tokens not supported for the browsable API
- django sessions should be used in development
- tokens are to be used in production
-
update
env.py
to containos.environ['DEV'] = '1'
-
install the following via the CLI:
pip3 install djangorestframework-simplejwt
-
in
settings.py
, add the following code to make the app run tokens or sessions authentication depending on the environment the app is running inREST_FRAMEWORK = { 'DEFAULT_AUTHENTICATION_CLASSES': [( 'rest_framework.authentication.SessionAuthentication' if 'DEV' in os.environ else 'dj_rest_auth.jwt_auth.JWTCookieAuthentication' )] }
-
next, enable token authentication by adding another variable in
settings.py
:REST_USE_JWT = True
-
also, make sure that the token authentication is sent over HTTPS only/is secure by adding this variable as well:
JWT_AUTH_SECURE = True
-
next, declare the names for the access and refresh tokens used by JWT framework by adding another 2 variables below the 2 above:
JWT_AUTH_COOKIE = 'my-app-auth' JWT_AUTH_REFRESH_COOKIE = 'my-refresh-token'
Great! Now we need to add the profile_id and profile_image to fields returned when requesting logged in user’s details. This way we’ll know which profile to link to and what image to show in the navigation bar for a logged in user.
-
in the main app (
drf_api
) create aserializers.py
-
inside it, paste in the following code:
from dj_rest_auth.serializers import UserDetailsSerializer from rest_framework import serializers class CurrentUserSerializer(UserDetailsSerializer): profile_id = serializers.ReadOnlyField(source='profile.id') profile_image = serializers.ReadOnlyField(source='profile.image.url') class Meta(UserDetailsSerializer.Meta): fields = UserDetailSerializer.Meta.fields + ( 'profile_id', 'profile_image' )
All we’re doing here is adding the profile_id and profile_image fields to the stock
UserDetailsSerializer
-
with the serializer created, head back to
settings.py
and add the following variable to overwrite the default serializer that handles user details:-
REST_AUTH_SERIALIZERS = { 'USER_DETAILS_SERIALIZER': 'drf_api.serializers.CurrentUserSerializer' }
-
-
with everything installed, run a migration again
-
and with everything migrated, update the requirements.txt file
pip3 freeze > requirements.txt
Ok, we’re finally finished setting up JSON Web Token authentication for our app.
Explanation | dj-rest-auth/ url | http | data | data received |
---|---|---|---|---|
To register, our users will send a POST request to ‘django rest auth register’ with their username, password and confirmed password. | registration/ | POST | username + password1 + password2 | |
To log in, our users will send a POST request to ‘login’ with their username and password. As mentioned, they will be issued an access and refresh token. | login/ | POST | username + password | access token + refresh token |
To log out, our users will just send a POST request to ‘logout’. | logout/ | POST | ||
To fetch user specific details, like user_id, profile_id and profile_image, | ||||
we’ll make a GET request to ‘user’ | user/ | GET | access token + refresh token | username + profile_id + profile_image |
To refresh user access tokens, | ||||
we’ll make POST requests to token/refresh and get a new one if the refresh token hasn’t expired. | token/refresh/ | POST | access token + refresh token | (new)access token |