DRF is an awesome tool for building web APIs the RESTful way: allowing you to interact with your database like so:
POST request to /api/articles with hdata payload to create new Article instance. PATCH request to /api/articles/1/ to partially update Article with pk of 1..save PUT request to /api/articles/1/ to completely replace Article with pk of 1. DELETE request to /api/articles/1/ to delete Article with pk of 1. GET request to /api/articles/1/ to retrieve Article with pk of 1. HEAD request to /api/articles/1/ to see if Article with pk of exists.
Gone are the days when we POST data like
POST request to /api/create_article with data payload POST request to /api/update_article with data payload POST request to /api/delete_article with data payload
The main advantage I see in RESTful is it gives us sane restrictions we must develop under and in doing so so helps organize our web apps – thereby avoid accidentally being “clever” and implementing a hard to maintain codebase. Since using DRF I found adding new features to my apps quicker, DRYer and with more readable code.
If you are a django user and interested in DRF, do pip install djangorestframework. For existing DRF users note make sure you have DRF >= 2.3.11, which now supports write_only_fields.
Defining the endpoint
Below we go through how to expose the User model to the web using a DRF endpoint to allow creating, updating, listing, deleting User objects. Note my folder structure is:
apps/ accounts/ urls.py api/ views.py serializers.py permissions.py
We need to create a view that will serve list and detail view of users:
from django.contrib.auth.models import User from rest_framework viewsets from rest_framework.permissions import AllowAny from .permissions import IsStaffOrTargetUser class UserView(viewsets.ModelViewSet): serializer_class = UserSerializer model = User def get_permissions(self): # allow non-authenticated user to create via POST return (AllowAny() if self.request.method == 'POST' else IsStaffOrTargetUser()),
We need to be careful with permissions – we dont want users to be able to view other user objects if they are not staff members.
from rest_framework import permissions class IsStaffOrTargetUser(permissions.BasePermission): def has_permission(self, request, view): # allow user to list all users if logged in user is staff return view.action == 'retrieve' or request.user.is_staff def has_object_permission(self, request, view, obj): # allow logged in user to view own details, allows staff to view all records return request.user.is_staff or obj == request.user
Next we define the serializer that will serialize Querysets and objects to JSON. We need to be careful on create of User object to handle passwords correctly, and on read not to serialize and return the password to the client.
from django.contrib.auth.models import User from rest_framework import serializers class UserSerializer(serializers.ModelSerializer): class Meta: model = User fields = ('password', 'first_name', 'last_name', 'email',) write_only_fields = ('password',) read_only_fields = ('is_staff', 'is_superuser', 'is_active', 'date_joined',) def restore_object(self, attrs, instance=None): # call set_password on user object. Without this # the password will be stored in plain text. user = super(UserSerializer, self).restore_object(attrs, instance) user.set_password(attrs['password']) return user
Now register the endpoint in the app’s urls.py:
from django.conf.urls import patterns, url, include from rest_framework import routers from . import api router = routers.DefaultRouter() router.register(r'accounts', api.views.UserView, 'list') urlpatterns = patterns( '', url(r'^api/', include(router.urls)), )
Using the endpoint
Below are examples of calling the endpoint using jQuery, showing the request and the data returned.
Create new user
var data = {username: 'new@user.com', password: '****', ...}; $.post('/api/accounts/', data).done(function(data){ console.log(data); }); {first_name: "New" last_name: "User", email: "new@user.com", id:4}
Update user details
var data = {email: 'new@user.co.uk'}; $.ajax({url: '/api/accounts/4', type: 'patch', data: data}).done(function(data){ console.log(data); }); {first_name: "New" last_name: "User", email: "new@user.co.uk", id:4}
List all users when logged in as staff
$.get('/api/accounts/').done(function(data){ console.log(data); }); [{first_name: "Richard" last_name: "Tier" email: "me@richardtier.com", id: 1}, {first_name: "John" last_name: "Doe", email: "Jon@Doe.com", id: 2}, {first_name: "Jane" last_name: "Doe", email: "jane@doe.com", id: 3}];
Retrieve own record when logged in as Jon@Doe.com
$.get('/api/accounts/2').done(function(data){ console.log(data); }); {first_name: "John" last_name: "Doe", email: "john@doe.com", id: 2}
Retrieve Jon@Doe.com’s record when NOT logged in as staff member and NOT user Jon@Doe.com
$.get('/api/accounts/1').fail(function(xhr){ console.log(JSON.parse(xhr.responseText)); }); {detail: "You do not have permission to perform this action."}
In a follow up post we cover checking username and password using DRF.
user.set_password(attrs[‘password’]) is not hashing the password
Thanks for highlighting. After reading docs I see set_password doesn’t save the User object and we must manually call user.save() https://docs.djangoproject.com/en/dev/ref/contrib/auth/#django.contrib.auth.models.User.set_password
def post_save(self, obj, created=False):
“””
On creation, replace the raw password with a hashed version.
“””
if created:
obj.set_password(obj.password)
obj.save()
this does the job… great article btw!
Great post.
Great! It works very well. No need to post_save. The password is hashed 🙂
what version of django and django rest framework are you using? When I tested I had to hash password.
Django 1.7c1 and DRF 2.3.14
also, thanks 🙂
Patching a user is giving out password as key error if I dont pass the password in data
odd. try checking if password field is present and only setting password if is present.
Great post my friend, help me a lot… I’m new on django and django rest framework now… maybe you can help me… I have a models called UserProfile with a OneToOneField to auth.User. Now, I need to create a new instance of userprofile when users craete a new user using the api… ¿how can I create this new instance?¿in the view?
Something like:
UserProfile.objects.create(
user=NewUser,
)
Thankyou, great post….
a common pattern is to use signals.
https://docs.djangoproject.com/en/dev/ref/signals/#django.db.models.signals.post_save
This lets you call a funcction when a database event occurs (such as pre_save or post_save on a model).
so do something like
@receiver(models.signals.post_save, sender=User)
def create_profile(sender, instance, created, **kwargs):
if not created:
return
UserProfile.objects.create(user=instance)
stik that in signals.py in your app, then import it from models.py in your app.
Thanks for the post, would you mind showing how to upgrade to DRF 3? restore_object has been replaced with update and create for example.
good idea. I will do a follow up post explaining how to do this on DRF3
It is probably not very safe to allow users to change their is_superuser or is_staff flag
very good point. I will add this to “read_only”
thanks man!
А good thought-out guide, thanks! I used DRF 3, so i ran into some troubles:
– get_permissions() must return tuple, so:
def get_permissions(self):
#allow non-authenticated user to create via POST
return (AllowAny() if self.request.method == ‘POST’ else IsStaffOrTargetUser(),)
– ModelViewSet must have queryset property like:
from django.contrib.auth import get_user_model
class UserViewSet(viewsets.ModelViewSet):
queryset = get_user_model().objects
…
– to mark fields as write-only serializer must have “extra_kwargs” property like:
extra_kwargs = {
‘password’: {‘write_only’: True},
}
By this way aren’t you allowing any malicious user to use all your server resources by constantly creating new users? How should you defend against it?
rate limiting will be helpful here.