Skip to content

Commit

Permalink
Added create use code and docs
Browse files Browse the repository at this point in the history
  • Loading branch information
shabda committed Mar 15, 2018
1 parent 57ed5b3 commit bd1d654
Show file tree
Hide file tree
Showing 4 changed files with 82 additions and 99 deletions.
153 changes: 57 additions & 96 deletions docs/access-control.rst
Original file line number Diff line number Diff line change
@@ -1,45 +1,32 @@
Adding Authentication
Access Control
=================================

In this chapter we will be dealing with the creation of URLs and authentication.
In this chapter, we will add access control to our APIs,
and add APIs to create and authenticate users.

Creating URLs
--------------
Right now our APIs are completely permissive. Anyone can create, access and delete anything.
We want to add these access controls.

It's time to wire up the views to their respective specific URLs. We shall do it in the following manner.

.. code-block:: python
- A user must be authenticated to access a poll or the list of polls.
- Only an authenicated users can create a poll.
- Only an authenticated user can create a choice.
- Authenticated users can create choices only for polls they have created.
- Authenticated users can delete only polls they have created.
- Only an authenticated user can vote. Users can vote for other people's polls.

from django.conf.urls import include, url
from django.contrib import admin
To enable thi access control, we need to add two more APIs

import pollsapi.views
- API to create a user, we will call this endpoint :code:`/users/`
- API to verify a user and get a token to identify them, we will call this endpoint :code:`/login/`

urlpatterns = [

url(r'^admin/', include(admin.site.urls)),
url(r'^polls/$', pollsapi.views.PollList.as_view()),
url(r'polls/(?P<pk>[0-9]+)/$', pollsapi.views.PollDetail.as_view()),
url(r'^create_user/$', pollsapi.views.UserCreate.as_view()),
url(r'^choices/(?P<pk>[0-9]+)/$', pollsapi.views.ChoiceDetail.as_view()),
url(r'^create_vote/$', pollsapi.views.CreateVote.as_view()),
]
In the above lines of code we have created URLs for all the views according to our requirement.

Authentication of the API
Creating a user
--------------------------

Right now any one can create a poll or vote for a choice, and our endpoints are openly accessable to the world. Let us restrict the access by adding the authenitcation.
The following are the on we are going to deal with:

- A user has to login to access a poll.
- Only an authenicated users can create a poll.
- Only an authenticated user can vote.
- Only an authenticated user can create a choice.


Initially we should need representaions for User and for that let us create one in the serializers.py. Add the following code snippet to our serializers file.
We will add an user serializer, which will allow creating. Add the following code to :code:`serializers.py`.

.. code-block:: python
Expand All @@ -59,33 +46,59 @@ Initially we should need representaions for User and for that let us create one
user.save()
return user
In the above lines of code we used the ModelSerializer method's 'create()' to save the 'User' instances.
We have overriden the ModelSerializer method's :code:`create()` to save the :code:`User` instances. We ensure that we set the password correctly using :code:`user.set_password`, rather than setting the raw password as the hash. We also don't want to get back the password in response which we ensure using :code:`extra_kwargs = {'password': {'write_only': True}}`.

Let us also add views to the User Serializer for creating and retrieving the user.
Let us also add views to the User Serializer for creating the user and connect it to the urls.py

.. code-block:: python
class UserCreate(generics.CreateAPIView):
"""
Create an User
"""
# in apiviews.py
# ...
from .serializers import PollSerializer, ChoiceSerializer, VoteSerializer, UserSerializer
# ...
class UserCreate(generics.CreateAPIView):
serializer_class = UserSerializer
# in urls.py
# ...
from .apiviews import PollViewSet, ChoiceList, CreateVote, UserCreate
class UserDetail(generics.RetrieveAPIView):
"""
Retrieve a User
"""
queryset = User.objects.all()
serializer_class = UserSerializer
urlpatterns = [
# ...
path("users/", UserCreate.as_view(), name="user_create"),
]
Let us create a URL for accessing the detail info about the user. For that access the urls.py file and wire up the following User URL.
We can test this api by posting to :code:`/users/` with this json.

.. code-block:: python
.. code-block:: json
{
"username": "nate.silver",
"email": "nate.silver@example.com",
"password": "FiveThirtyEight"
}
Which give back this response.

.. code-block:: json
{
"username": "nate.silver",
"email": "nate.silver@example.com"
}
Try posting the same json, and you will get a error response (HTTP status code 400)

.. code-block:: json
{
"username": [
"A user with that username already exists."
]
}
url(r'^users/(?P<pk>[0-9]+)/$', pollsapi.views.UserDetail.as_view()),
Authentication scheme setup
-----------------------------
Expand Down Expand Up @@ -133,55 +146,3 @@ Also, dont forget to give excemption to UserCreate view by overriding the global
serializer_class = UserSerializer
All done, so from now the user needs to be an 'authenticated user' to access our poll and the poll data.

Exceptional handling
---------------------

Now, let us deal with exception handling which will make our code to work perfect at all situations. Take an instance of a user trying to select a choice that is not availble in choice list and our application should not get freezed or turn buggy. At this point we can make use of 'ValidationError' class which can be used for serializer and field validations.

The model Serializer class in Django Rest Framework has the default implementations of '.create()' and '.update()'. We can make use fo the '.create()' here. Let us do it right away in the pollsapi/serializers.py.

.. code-block:: python
class VoteSerializer(serializers.ModelSerializer):
class Meta:
model = Vote
validators=[
UniqueTogetherValidator(
queryset=Vote.objects.all(),
fields=('poll', 'voted_by'),
message="User already voted for this poll"
)
]
def create(self, validated_data):
poll = validated_data["poll"]
choice = validated_data["choice"]
if not choice in poll.choices.all():
raise serializers.ValidationError('Choice must be valid.')
vote = super(VoteSerializer, self).create(validated_data)
return vote
In the above lines of code in the 'create()' we were checking whether the selected choice is a valid one or not. And if turns to be false a validation error will be raised.

We have got another place where we need to handle an exception. If the user forgot to create the choices while starting a new poll an exception needs to be raised that the choices needs to be created as well. For that, make the below changes in the PollSerializer method in the pollsapi/serializers.py

.. code-block:: python
class PollSerializer(serializers.ModelSerializer):
choices = ChoiceSerializer(many=True, read_only=True, required=False)
class Meta:
model = Poll
def create(self, validated_data):
choice_strings = self.context.get("request").data.get("choice_strings")
if not choice_strings:
raise serializers.ValidationError('choice_strings needed.')
poll = super(PollSerializer, self).create(validated_data)
for choice in choice_strings:
Choice.objects.create(poll=poll, choice_text=choice)
return poll
So the above fixes makes sure that no bugs comes to light and turns the code to run smooth.

6 changes: 5 additions & 1 deletion pollsapi/polls/apiviews.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from rest_framework import viewsets

from .models import Poll, Choice
from .serializers import PollSerializer, ChoiceSerializer, VoteSerializer
from .serializers import PollSerializer, ChoiceSerializer, VoteSerializer, UserSerializer


class PollViewSet(viewsets.ModelViewSet):
Expand All @@ -31,3 +31,7 @@ def post(self, request, pk, choice_pk):
return Response(serializer.data, status=status.HTTP_201_CREATED)
else:
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)


class UserCreate(generics.CreateAPIView):
serializer_class = UserSerializer
18 changes: 18 additions & 0 deletions pollsapi/polls/serializers.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from django.contrib.auth.models import User

from rest_framework import serializers

from .models import Poll, Choice, Vote
Expand Down Expand Up @@ -25,4 +27,20 @@ class Meta:
fields = '__all__'


class UserSerializer(serializers.ModelSerializer):

class Meta:
model = User
fields = ('username', 'email', 'password')
extra_kwargs = {'password': {'write_only': True}}

def create(self, validated_data):
user = User(
email=validated_data['email'],
username=validated_data['username']
)
user.set_password(validated_data['password'])
user.save()
return user


4 changes: 2 additions & 2 deletions pollsapi/polls/urls.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from django.urls import path

from .apiviews import PollViewSet, ChoiceList, CreateVote
from .apiviews import PollViewSet, ChoiceList, CreateVote, UserCreate

from rest_framework.routers import DefaultRouter

Expand All @@ -9,9 +9,9 @@


urlpatterns = [
path("users/", UserCreate.as_view(), name="user_create"),
path("polls/<int:pk>/choices/", ChoiceList.as_view(), name="polls_list"),
path("polls/<int:pk>/choices/<int:choice_pk>/vote/", CreateVote.as_view(), name="polls_list"),

]

urlpatterns += router.urls

0 comments on commit bd1d654

Please sign in to comment.