Efie
Efie

Reputation: 1680

Customizing permissions and responses for detail routes in Django rest framework

Overview - I am creating a Django REST API that returns data from nested url routes. The best way I have found to do this so far is by manually adding in the url regexes to the urls.py and then using @detail_route in my views to retrieve the filtered serializer data.

Right now I have user objects and goal objects that will need different data responses based on authentication, etc...

How do I customize the detail routes to do this? For example:

If a user is an admin they can use the 'post' method at the /api/v2/users url. If they are not authenticated they get a bad request 400 response.

If a user is an admin they can use the 'get' method to retrieve all users names, emails, and passwords, but if they are not they can only get usernames.

urls.py

urlpatterns = [

    url(r'^api/v2/users/$',
        UserViewSet.as_view({'get': 'users', 'post': 'users', 'put': 'users',
                             'patch': 'users', 'delete': 'users'}),
        name='user_list'),

    url(r'^api/v2/user/(?P<uid>\d+)/goals/$',
        UserViewSet.as_view({'get': 'user_goals', 'post': 'user_goals', 'put': 'user_goals',
                             'patch': 'user_goals', 'delete': 'user_goals'}),
        name='user_goals_list'),
]

serializers.py

class GoalSerializer(serializers.ModelSerializer):
    class Meta:
        model = Goal
        fields = ('id', 'user_id', 'name', 'amount',
                  'start_date', 'end_date', )


class UserSerializer(serializers.ModelSerializer):
    class Meta:
        model = User
        fields = ('username', 'email', 'id', 'password')
        read_only_fields = ('id', )
        extra_kwargs = {'password': {'write_only': True}}

views.py

class UserViewSet(viewsets.ModelViewSet):
    queryset = User.objects.all()
    serializer_class = serializers.UserSerializer

    @detail_route(methods=['get', 'post', 'delete', 'put', 'patch', ])
    def users(self, request):
        users = User.objects.all()
        serializer = serializers.UserSerializer(
            users, many=True
        )
        return Response(serializer.data)

    @detail_route(methods=['get', 'post', 'delete', 'put', 'patch', ])
    def user_goals(self, request, uid):
        goals = Goal.objects.filter(user_id=uid)
        serializer = serializers.GoalSerializer(
            goals, many=True
        )
        return Response(serializer.data)

    @detail_route(methods=['get', 'post', 'delete', 'put', 'patch', ])
    def user_goal_detail(self, request, uid, gid):
        goal = Goal.objects.filter(user_id=uid, id=gid)
        serializer = serializers.GoalSerializer(
            goal, many=True
        )
        return Response(serializer.data)

Upvotes: 1

Views: 2113

Answers (1)

henriquesalvaro
henriquesalvaro

Reputation: 1272

As far as the nested routing goes, I suggest you take a look into the drf-nested-routers package or similar, it tends to make your life easier regarding the routing, look into the SimpleRouter and NestedSimpleRouter classes.

If a user is an admin they can use the 'post' method at the /api/v2/users url. If they are not authenticated they get a bad request 400 response.

The @detail_route decorator can receive a permission_classes parameter, where you can specify the permissions required to perform the actions declared, much like the ViewSet you're using.

However, your example shows a ModelViewSet for the User model, meaning you already have multiple actions exposed, as well as multiple GenericViewSet related bonuses (get_serializer, get_object, etc):

class ModelViewSet(mixins.CreateModelMixin,
               mixins.RetrieveModelMixin,
               mixins.UpdateModelMixin,
               mixins.DestroyModelMixin,
               mixins.ListModelMixin,
               GenericViewSet):
"""
A viewset that provides default `create()`, `retrieve()`, `update()`,
`partial_update()`, `destroy()` and `list()` actions.
"""
pass

So, for example, if you wanted to perform the GET /api/v2/users/, by linking it either through the router or through {'get': 'list'} on urls.py, you could override the get_serializer_class method based on the user:

def get_serializer_class(self):
    """
    Return the class to use for the serializer.
    Defaults to using `self.serializer_class`.

    You may want to override this if you need to provide different
    serializations depending on the incoming request.

    (Eg. admins get full serialization, others get basic serialization)
    """
    assert self.serializer_class is not None, (
        "'%s' should either include a `serializer_class` attribute, "
        "or override the `get_serializer_class()` method."
        % self.__class__.__name__
    )

    return self.serializer_class

In that case, you can also play around with the permission_classes parameter of the UserViewSet, by allowing anyone using SAFE_METHODS, otherwise checking for admin status:

from rest_framework.permissions import BasePermission, SAFE_METHODS

class IsAdminOrReadOnly(BasePermission):

    def has_permission(self, request, view):
        if request.method in SAFE_METHODS:
            return True

        return request.user and request.user.is_staff

class UserViewSet(viewsets.ModelViewSet):
    queryset = User.objects.all()
    serializer_class = serializers.UserSerializer
    permission_classes = (IsAdminOrReadOnly,)
    ...

I'm probably late to the party, but hopefully this will be helpful to others in the future.

Upvotes: 6

Related Questions