Django - item created by logged user ViewSet - python

What I want: A logged user creates an item which is stored in the database with that user ForeignKey.
I have 2 codes: both should do the same thing (create item linked to current user), but one of them works and the other one doesn't. GET requests work, I'm focusing on POST requests.
First code (POST working):
class UserPortfolio(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
name = models.CharField(max_length=200, blank=False, null=False)
slug = models.CharField(max_length=200, blank=False, null=False)
class UserPortfolioSerializer(serializers.ModelSerializer):
class Meta:
model = UserPortfolio
exclude = ('id', 'user')
class UserPortfolioViewSet(viewsets.ModelViewSet):
serializer_class = serializers.UserPortfolioSerializer
queryset = UserPortfolio.objects.all()
permission_classes = [IsAuthenticated]
def get_queryset(self):
return UserPortfolio.objects.filter(user=self.request.user).order_by('last_updated')
def perform_create(self, serializer):
serializer.save(user=self.request.user)
Second code (POST not working):
class ExchangeConnection(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
portfolios = models.ManyToManyField(UserPortfolio, blank=True)
exchange = models.ForeignKey(Exchange, on_delete=models.CASCADE)
last_updated = models.DateTimeField(auto_now=True)
class Meta:
db_table = 'exchange_connection'
verbose_name_plural = "Exchange Connection"
def __str__(self):
return self.user.username
class ExchangeConnectionSerializer(FlexFieldsModelSerializer):
class Meta:
model = ExchangeConnection
fields = '__all__'
expandable_fields = {'exchange': ExchangeSerializer,
'portfolios': (UserPortfolioSerializer, {'many': True})}
class UserExchangeConnectionsViewSet(viewsets.ModelViewSet):
serializer_class = serializers.ExchangeConnectionSerializer
queryset = ExchangeConnection.objects.all()
permission_classes = [IsAuthenticated]
def get_queryset(self):
return ExchangeConnection.objects.filter(user=self.request.user).order_by('last_updated')
#action(
detail=True,
methods=['post'],
url_path='create-exchange-connection',
)
def post(self, request):
serializer = serializers.ExchangeConnectionPostSerializer(user=request.user)
if serializer.is_valid():
serializer.save()
return Response({'status': 'Exchange created.'})
else:
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
My urls:
from django.urls import path, include
from api_v1.exchanges.views import UserPortfolioViewSet, UserExchangeConnectionsViewSet, ExchangesViewSet
from rest_framework.routers import DefaultRouter
# Create a router and register our viewsets with it.
router = DefaultRouter()
router.register(r'portfolios', UserPortfolioViewSet)
router.register(r'exchange-connections', UserExchangeConnectionsViewSet)
router.register(r'exchanges', ExchangesViewSet)
urlpatterns = [
path('', include(router.urls)),
]
The error I get when I POST is:
TypeError: post() got an unexpected keyword argument 'pk'

#action(
detail=True,
methods=['post'],
url_path='create-exchange-connection',
)
def post(self, serializer):
serializer = serializers.ExchangeConnectionPostSerializer(user=self.request.user)
if serializer.is_valid():
serializer.save()
return Response({'status': 'Exchange created.'})
else:
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
you have used detail = True in the action, which means you are making a retrieve request for a particular object, so you must pass a lookup_field value which is by default pk.
If you just want to make a create request then you should set detail=False.
Updated code.
#action(
detail=False,
methods=['post'],
url_path='create-exchange-connection',
)
def post(self, serializer):
serializer = serializers.ExchangeConnectionPostSerializer(user=self.request.user)
if serializer.is_valid():
serializer.save()
return Response({'status': 'Exchange created.'})
else:
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

As documented
Like regular actions, extra actions may be intended for either a
single object, or an entire collection. To indicate this, set the
detail argument to True or False. The router will configure its URL
patterns accordingly. e.g., the DefaultRouter will configure detail
actions to contain pk in their URL patterns.
so you should set detail=False
#action(
detail=False,
methods=['post'],
url_path='create-exchange-connection',
)

Related

Django REST multiple permission class not working

I have two permission class. IsAuthorGroup will check if the user belongs from the author group and IsOwnerOrReadOnly will restrict the user to performing post and delete if he is not the object's owner.
But the problem is anyone from IsAuthorGroup performing post and delete request event he isn't own of the object.
How to restrict anyone from IsAuthorGroup performing post and delete request if he isn't owner of the object?
here is my code:
class IsAuthorGroup(permissions.BasePermission):
def has_permission(self, request, view):
if request.user and request.user.groups.filter(name='AuthorGroup'):
return True
return False
class IsOwnerOrReadOnly(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
if request.method in permissions.SAFE_METHODS:
return True
# Write permissions are only allowed to the owner of the blog.
return obj.author == request.user or request.user.is_superuser
class BlogViewSet(viewsets.ModelViewSet):
queryset = Blog.objects.all()
serializer_class = BlogSerializer
pagination_class = BlogPagination
lookup_field = 'blog_slug'
permission_classes = [IsOwnerOrReadOnly & IsAuthorGroup]
my serializer.py
class BlogSerializer(serializers.ModelSerializer):
author_first_name = serializers.CharField(
source="author.first_name", required=False)
author_last_name = serializers.CharField(
source="author.last_name", required=False)
class Meta:
model = Blog
exclude = ("author", "blog_is_published")
lookup_field = 'blog_slug'
extra_kwargs = {
'url': {'lookup_field': 'blog_slug'}
}

Django filters with APIVIew return the complete queryset

I'm trying to use django filter with APIVIew like I saw in this post and I'm geting an unexpected behavior:
If the word passed on filter doesn't exists, it return nothing - OK
If the word passed on filter exists, it return the complete queryset.
I tried to use filters with ModelViewSet before and it works, but I have no success with APIView. I'm using the following code:
views:
from rest_framework import status
from rest_framework.response import Response
from rest_framework.views import APIView
from core.models import Profile
from core.serializers import ProfileSerializer
from django_filters.rest_framework import DjangoFilterBackend
class ProfileFilter(DjangoFilterBackend):
def filter_queryset(self, request, queryset, view):
filter_class = self.get_filter_class(view, queryset)
if filter_class:
return filter_class(request.query_params, queryset=queryset, request=request).qs
return queryset
class ListProfileView(APIView):
serializer_class = ProfileSerializer
filter_fields = ('nome', 'link')
queryset = Profile.objects.all()
def get(self, request, format=None):
queryset = Profile.objects.all()
ff = ProfileFilter()
filtered_queryset = ff.filter_queryset(request, queryset, self)
if filtered_queryset.exists():
serializer = self.serializer_class(queryset, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
else:
return Response([], status=status.HTTP_200_OK)
def post(self, request, format=None):
serializer = self.serializer_class(data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
model:
from django.db import models
class Profile(models.Model):
nome = models.CharField(max_length=200, null=False, blank=False)
link = models.CharField(max_length=200, null=False, blank=False)
serializers:
from rest_framework.serializers import ModelSerializer
from core.models import Profile
class ProfileSerializer(ModelSerializer):
class Meta:
model = Profile
fields = '__all__'

Filtering by Foreign Key in ViewSet, django-rest-framework

I want my api to return certain objects from a database based on the foreign key retrieved from the url path. If my url looks like api/get-club-players/1 I want every player object with matching club id (in this case club.id == 1). I'm pasting my code down below:
models.py
class Club(models.Model):
name = models.CharField(max_length=25)
owner = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True)
def __str__(self):
return self.name
class Player(models.Model):
name = models.CharField(max_length=30)
club = models.ForeignKey(Club, on_delete=models.SET_NULL, blank=True, null=True)
def __str__(self):
return self.name
serialziers.py
class ClubSerializer(serializers.ModelSerializer):
class Meta:
model = Club
fields = 'id', 'owner', 'name'
class PlayerSerializer(serializers.ModelSerializer):
class Meta:
model = Player
fields = 'id', 'name', 'offense', 'defence', 'club', 'position'
views.py, This is the part where I get the most trouble with:
class ClubViewSet(viewsets.ModelViewSet):
queryset = Club.objects.all()
serializer_class = ClubSerializer
class PlayerViewSet(viewsets.ModelViewSet):
queryset = Player.objects.all()
serializer_class = PlayerSerializer
class GetClubPlayersViewSet(viewsets.ViewSet):
def list(self, request):
queryset = Player.objects.all()
serializer = PlayerSerializer(queryset, many=True)
def retrieve(self,request, clubId):
players = Player.objects.filter(club=clubId, many=True)
if not players:
return JsonResponse({'error': "No query found!"})
else:
serializer = PlayerSerializer(players)
return Response(serializer.data)
urls.py
from rest_framework import routers
from django.urls import path, include
from .views import (GameViewSet, PlayerViewSet, ClubViewSet,
GetClubPlayersViewSet, create_club, set_roster)
router = routers.DefaultRouter()
router.register(r'clubs', ClubViewSet, basename="clubs")
router.register(r'players', PlayerViewSet, basename="players")
router.register(r'get-club-players', GetClubPlayersViewSet, basename="club-players")
urlpatterns = [
path('', include(router.urls)),
]
EDIT:
Now views.py looks like that:
class GetClubPlayersViewSet(viewsets.ViewSet):
queryset = Player.objects.all()
def list(self, request):
serializer = PlayerSerializer(self.queryset, many=True)
return Response(serializer.data)
def retrieve(self, request, *args, **kwargs):
clubId = kwargs['get-club-players']
players = Player.objects.filter(club=clubId, many=True)
if not players:
return JsonResponse({'error': "No query found!"})
else:
serializer = PlayerSerializer(players)
return Response(serializer.data)
http://127.0.0.1:8000/api/get-club-players/ returns all of the player objects, but when I ad a clubId into url I get this error:
EDIT 2:
class GetClubPlayersViewSet(viewsets.ViewSet):
queryset = Player.objects.all()
def retrieve(self, request, *args, **kwargs):
queryParams = self.request.GET.get('abc')
if queryParams is None:
queryset = Player.objects.none()
else:
queryset = Player.objects.filter(club = queryParams)
serializer = PlayerSerializer(queryset)
return Response(serializer.data)
def list(self, request):
serializer = PlayerSerializer(self.queryset, many=True)
return Response(serializer.data)
You can get url parameters using kwargs attribute. You will need to modify the signature of your retrieve method for it.
def retrieve(self, request, *args, **kwargs):
clubId = kwargs['get-club-players']
players = Player.objects.filter(club=clubId, many=True)
....
EDIT
For the queryset error, it is due to DRF requiring either the queryset class attribute or implementation of get_queryset() function. In your case, you can get around it like this:
class GetClubPlayersViewSet(viewsets.ViewSet):
queryset = Player.objects.all()
def list(self, request):
serializer = PlayerSerializer(self.queryset, many=True)
So you can define your queryset like -
def get_queryset(self):
queryParams == self.request.GET.get('abc') # get queryparameter from url
if queryParams is None:
#queryset = anyModel.objects.all()
queryset = anyModel.objects.none()
else:
queryset = anyModel.objects.filter(anyProperty = queryParams)
return queryset
and your url will be like -
api/get-club-players/?abc=1
this abc can be id or any other property from the model.
Use this get_queryset logic in your retrieve method.
rest_framework.viewsets.ViewSet has an attribute named lookup_field which you can override. By default the value of lookup_field is id.
When adding the viewset in router, the lookup_field is added as the argument name in the url (e.g. /api/get-club-players/:id/).
You can either override the lookup_field of GetClubPlayersViewSet or access the correct kwargs key by changing
clubId = kwargs['get-club-players'] to clubId = kwargs['id']
Or a bit of both:
class GetClubPlayersViewSet(viewsets.ViewSet):
lookup_field = "clubId"
queryset = Player.objects.all()
# ....
def retrieve(self, request, *args, **kwargs):
clubId = kwargs[self.lookup_field]
players = Player.objects.filter(club=clubId, many=True)
if not players:
return JsonResponse({'error': "No query found!"})
else:
serializer = PlayerSerializer(players)
return Response(serializer.data)

Django Rest Framework: Access to passed arguments from views in serializers

Before asking this question, I have seen the following links but they don't help me at all:
pass extra arguments to serializer
pass request context to serializer from viewset
pass context from one serializer to another
I have an author model that has foreign key to default django user model:
apps/author/models.py
class Author(models.Model):
user = models.OneToOneField(
User,
related_name='author',
on_delete=models.CASCADE,
default="",
)
is_author = models.BooleanField(
default=True,
)
full_name = models.CharField(
max_length=100,
default="",
)
def __str__(self):
return self.user.username
Post model has a foreign key to Author.
apps/posts/models.py
class Post(models.Model):
author = models.ForeignKey(
Author,
related_name="posts",
on_delete=models.CASCADE,
)
title = models.TextField(
null=True,
blank=True,
)
content = models.TextField(
null=True,
blank=True,
)
is_draft = models.BooleanField(
default=True
)
created_at = models.DateTimeField(
auto_now_add=True,
null=True,
)
published_at = models.DateField(
null=True,
blank=True,
default=None,
)
def __str__(self):
return str(self.id) + ", " + self.title
Problem Definition: In order to create a new post, I am getting the current user from self.request.user in views, and pass it to the PostSerializer. But whenever I want to create a new post using the following request to localhost:8000/posts/ I have got an error:
# I also added JWT authorization header to the postman! and it doesn't have any problem at this level
{
"title": "",
"content": ""
}
error
This is what I have done in apps/posts/views.py:
def get_serializer_context(self):
context = super().get_serializer_context()
context["user"] = self.request.user
context["author"] = Author.objects.get(user=context["user"])
print(context["author"])
return context
print(context["author"]) works well and prints out the current author. The problem is that I can't get it in serializers.
class PostSerializer(serializers.ModelSerializer):
# author = serializers.SerializerMethodField('get_author')
#
# def get_author(self, obj):
# print('current author', self.context["author"])
# return self.context["author"]
class Meta:
model = Post
fields = '__all__'
extra_fields = ['author']
#def create(self, validated_data):
#print(self.context["author"])
#print(self.context)
PS: The comments are the ways I have tried but the error is still occurred. How can I fix the problem?
Maybe you should add "required=False" in author field of PostSerializer, this will avoid "this field is required" error.
class PostSerializer(serializers.ModelSerializer):
author = serializers.PrimaryKeyRelatedField(required=False)
And what view do you use? If you use GenericAPIView and its subclasses, context will pass to serializer, the default get_serializer_context will pass request by default. if not you should pass context manually.
class PostSerializer(serializers.ModelSerializer):
author = serializers.PrimaryKeyRelatedField(required=False)
def create(self, validated_data):
request = self.context.get('request')
author = Author.objects.get(user=request.user)
# ....
class PostView(mixins.CreateModelMixin, viewsets.GenericViewSet):
serializer_class = PostSerializer
def create(self, request, *args, **kwargs):
super().create(request, *args, **kwargs)
UPDATE:
I was wrong, there is a simpler solution: https://stackoverflow.com/a/38167148/7285863
That should be simplest way.
class PostViewSet(viewsets.ModelViewSet):
# ... other implementations
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data={"author": request.user.author.id, **request.data})
serializer.is_valid(raise_exception=True)
self.perform_create(serializer)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
def update(self, request, pk, *args, **kwargs):
try:
post = Post.objects.get(pk=pk)
except Post.DoesNotExist:
raise NotFound("Post doesn't exist.")
if post.author != request.user.author:
raise PermissionDenied('Permission Denied!')
serializer = PostSerializer(post, data={"author": request.user.author.id, **request.data})
serializer.is_valid(raise_exception=True)
self.perform_update(serializer)
return Response(serializer.data)
It's default method except passing author data.
You may need to check user has author relation.

Django Rest Framework: Custom IsReadOnly Permission

I create a custom permission which authorizes GET, HEAD and OPTION for everyone and which authorizes NO other requests.
But my code doesn't work. I can make a POST request despite my permission ...
Anyone have a idea to solve my problem ?
My views.py:
class IsReadOnly(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
if request.method in permissions.SAFE_METHODS:
return True
return False
class ViewPollViewSet(viewsets.ModelViewSet):
permission_classes = [
IsReadOnly,
]
serializer_class = ViewPollSerializer
queryset = ViewPoll.objects.all()
My serializers.py:
class ViewPollSerializer(serializers.ModelSerializer):
class Meta:
model = ViewPoll
fields = '__all__'
My models.py:
class ViewPoll(models.Model):
''' view poll '''
class Meta:
unique_together = ('poll', 'user')
poll = models.ForeignKey(Poll, on_delete=models.CASCADE, related_name="views", null=True)
user = models.ForeignKey(User,on_delete=models.CASCADE, related_name="views_poll", null=True)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return str(self.poll.question)[:30]
Call has_permission(...) method instead of has_object_permission(...) method
class IsReadOnly(permissions.BasePermission):
def has_permission(self, request, view):
if request.method in permissions.SAFE_METHODS:
return True
return False

Categories