How to override the update action in django rest framework ModelViewSet? - python

These are the demo models
class Author(models.Model):
name = models.CharField(max_lenght=5)
class Post(models.Model):
author = models.ForeignKey(Author, on_delete=models.CASCADE)
title = models.CharField(max_lenght=50)
body = models.TextField()
And the respective views are
class AuthorViewSet(viewsets.ModelViewSet):
queryset = Author.objects.all()
serializer_class = AuthorSerializer
class PostViewSet(viewsets.ModelViewSet):
queryset = Post.objects.all()
serializer_class = PostStatSerializer
I am trying to perform an update/put action on PostViewSet and which is succesfull, but I am expecting different output. After successful update of Post record, I want to send its Author record as output with AuthorSerializer. How to override this and add this functionality?

You can override update method for this:
class PostViewSet(viewsets.ModelViewSet):
queryset = Post.objects.all()
serializer_class = PostStatSerializer
def update(self, request, *args, **kwargs):
partial = kwargs.pop('partial', False)
instance = self.get_object()
serializer = self.get_serializer(instance, data=request.data, partial=partial)
serializer.is_valid(raise_exception=True)
self.perform_update(serializer)
# this will return autor's data as a response
return Response(AuthorSerializer(instance.parent).data)

I figured out some less code fix for my issue.
class PostViewSet(viewsets.ModelViewSet):
queryset = Post.objects.all()
serializer_class = PostStatSerializer
def update(self, request, *args, **kwargs):
super().update(request, *args, **kwargs)
instance = self.get_object()
return Response(AuthorSerializer(instance.author).data)

Related

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)

How can I make only superusers to edit model

I have 2 models: Recipe and Ingridient and I want that only superusers will be able to edit model Ingridient, normal users can only view ingridients and are not able to edit them. Right now is set that way that if you are logged in you have all permissions, but I want that only superusers have ability to edit Ingridients and normal logged in users are able to add recipes (not able to edit ingridients).
#views.py
class ReceptViewRetrieveUpdateDestroy(generics.RetrieveUpdateDestroyAPIView):
queryset = Recipe.objects.all()
lookup_field = 'id'
serializer_class = RecipeSerializer
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
def delete(self, request, *args, **kwargs):
try:
id = request.data.get('id', None)
response= super().delete(request, *args, **kwargs)
if response.status_code == 204:
from django.core.cache import cache
cache.delete("{}".format(id))
return response
except Exception as e:
return response({
"Message":"Failed"
})
def update(self, request, *args, **kwargs):
response = super().update(request, *args, **kwargs)
if response.status_code == 200:
mydata = response.data
from django.core.cache import cache
cache.set("ID :{}".format(mydata.get('id', None)),{
'title':mydata["title"],
'description':mydata["description"],
'image':mydata["image"],
'galery':mydata["galery"],
'kitchen_type':mydata["kitchen_type"],
'num_persons':mydata["num_persons"],
'preparation_steps':mydata["preparation_steps"]
})
return response
class SestavinaViewRetrieveUpdateDestroy(generics.RetrieveUpdateDestroyAPIView):
queryset = Ingridient.objects.all()
lookup_field = 'id'
serializer_class = IngridientSerializer
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
def delete(self, request, *args, **kwargs):
try:
id = request.data.get('id', None)
response= super().delete(request, *args, **kwargs)
if response.status_code == 204:
from django.core.cache import cache
cache.delete("{}".format(id))
return response
except Exception as e:
return response({
"Message":"Failed"
})
def update(self, request, *args, **kwargs):
response = super().update(request, *args, **kwargs)
if response.status_code == 200:
mydata = response.data
from django.core.cache import cache
cache.set("ID :{}".format(mydata.get('id', None)),{
'name':mydata["name"],
'quantity':mydata["quantity"],
'calories':mydata["calories"],
'protein':mydata["protein"],
'carbon':mydata["carbon"],
'fiber':mydata["fiber"],
'fat':mydata["fat"],
'saturated_fat':mydata["saturated_fat"]
})
return response
class RecipeListView(generics.ListCreateAPIView):
model = Recipe
serializer_class = RecipeSerializer
queryset = Recipe.objects.all()
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
class IngridientsListView(generics.ListCreateAPIView):
model = Ingridient
serializer_class = IngridientSerializer
queryset = Ingridient.objects.all()
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
#models.py
class Ingridient(models.Model):
name = models.CharField(default='', max_length=20)
quantity = models.IntegerField(default=0)
calories = models.IntegerField(default=0)
protein = models.IntegerField(default=0)
carbon = models.IntegerField(default=0)
fiber = models.IntegerField(default=0)
fat = models.IntegerField(default=0)
saturated_fat = models.IntegerField(default=0)
def __str__(self):
return self.name
class Recipe(models.Model):
title = models.CharField(default='', max_length=60)
description = models.TextField(default='',max_length=1000)
image = models.ImageField(default='',upload_to='pics', blank=True)
galery = models.ImageField(default='', blank=True)
kitchen_type = models.CharField(default='',max_length=35)
num_persons = models.IntegerField(default=0)
preparation_steps = models.TextField(default='')
Ingridients = models.ManyToManyField(Ingridient)
def __str__(self):
return self.title
#serializers.py
class IngridientSerializer(serializers.ModelSerializer):
class Meta:
model = Ingridient
fields = ('name', 'quantity', 'calories', 'protein', 'carbon', 'fiber', 'fat',
'saturated_fat')
class RecipeSerializer(serializers.ModelSerializer):
Ingridients = IngridientSerializer(many=True)
class Meta:
model = Recipe
fields = ('title', 'description', 'image', 'galery', 'kitchen_type', 'num_persons',
'preparation_steps', 'Ingridients')
If you're willing to set up Django model permissions for your Ingredient model instead (i.e. add an "Ingredient Editors" group, and add all superusers to that group, then allow those users to edit Ingredients), you can use the DjangoModelPermissions permissions class instead of IsAuthenticatedOrReadOnly.
If you really do simply want to give all superusers write permissions, you can adapt DRF's IsAuthenticatedOrReadOnly permission class
to something like
class IsAdminOrReadOnly(BasePermission):
def has_permission(self, request, view):
return bool(
request.method in SAFE_METHODS or
request.user and
request.user.is_superuser
)
and then use that permission class instead.

Filtering querysets for nested models

I'm making a News App API and I want to create an APIView with comments for a speicific Post that also lets users posting comments for the specific post.
These are my models (simplified):
Post:
class Post(models.Model):
title = models.CharField(max_length=250)
text = models.TextField()
Comment:
class Comment(models.Model):
post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name='comments')
author = models.CharField(max_length=200)
text = models.TextField()
And view:
class CommentList(generics.ListCreateAPIView):
queryset = Comment.objects.filter(post=???)
serializer_class = CommentSerializer
EDIT: I would also like my url path to look like this (or similar):
urlpatterns = [
...
path('posts/<int:pk>/comments/', CommentList.as_view())
]
My questions:
How do I create a list of comments for an instance of Post?
Is it the correct approach or should I try something else?
If your url for comments is something like: /posts/post_id/comments/
# serializer
class CommentSerializer(serializers.ModelSerializer):
author = SomeAuthorSerializer(read_only=True)
class Meta:
model = Comment
fields = ('author', 'text')
# view
class CommentViewSet(viewsets.GenericViewSet,
mixins.CreateModelMixin,
mixins.ListModelMixin):
queryset = Comment.objects
serializer_class = CommentSerializer
def initial(self, request, *args, **kwargs):
super().initial(request, *args, **kwargs)
try:
self.post = Post.objects.get(pk=self.request.query_params.get('post_id'))
# Prefetch post object in this situation let you check permissions
# eg.:
if self.post.author != self.request.user:
raise PermissionDenied()
# remember that permission classes should be used !
except Post.DoesNotExist:
raise NotFound()
# It will filter your comments
def filter_queryset(self, queryset):
queryset = queryset.filter(post=self.post)
return super().filter_queryset(queryset)
# It will automatically bypass post object and author
# to your serializer create method
def perform_create(self, serializer):
serializer.save(post=self.post, author=self.request.user)
This is the solution that worked for me:
class CommentByPostList(generics.ListCreateAPIView):
queryset = Comment.objects.all()
serializer_class = CommentListSerializer
def get_queryset(self):
return Comment.objects.filter(post=self.kwargs['pk'])
Create a PostDetailView to GET a specific post then have the serializer return the comments for that Post.
# serializers.py
class CommentSerializer(serializers.ModelSerializer):
class Meta:
model = Comment
fields = '__all__'
class PostSerializer(serializers.ModelSerializer):
comments = CommentSerializer(many=True)
class Meta:
model = Post
fields = ('title', 'text', 'comments') # Comments field is recognized by the related_name set in your models.
# views.py
class PostDetailView(generics.RetreiveApiView):
permission_classes = (IsAuthenticated,)
serializer_class = PostSerializer
queryset = Post.objects.all()

private and public field in Django REST Framework

I want to have a list of public and private recipes, hiding all private recipes unless it's owner's. I created a manager for this:
class RecipeManager(models.Manager):
def public_recipes(self, *args, **kwargs):
return super(RecipeManager, self).filter(private=False)
def private_recipes(self, *args, **kwargs):
user = kwargs.pop('user')
return super(RecipeManager, self).filter(private=True, user=user)
class Recipe(models.Model):
name = models.CharField(max_length=100)
recipe = models.CharField(max_length=200)
private = models.BooleanField(default=False)
views.py:
class RecipeViewSet(viewsets.ModelViewSet):
queryset = Recipe.objects.all()
serializer_class = RecipeSerializer
permission_classes = (AllowAny,)
serializers.py:
class RecipeSerializer(serializers.ModelSerializer):
class Meta:
model = Recipe
fields = ('id', 'name', 'recipe', 'total_ingredients')
depth = 1
So, where can I use the methods public_recipes, private_recipes or is there a better solution for this?
Firstly, you may want to set your custom manager as the default manager of your Recipe model, like so:
class RecipeManager(models.Manager):
def public_recipes(self, *args, **kwargs):
return super(RecipeManager, self).filter(private=False)
def private_recipes(self, *args, **kwargs):
user = kwargs.pop('user')
return super(RecipeManager, self).filter(private=True, user=user)
class Recipe(models.Model):
name = models.CharField(max_length=100)
recipe = models.CharField(max_length=200)
private = models.BooleanField(default=False)
objects = RecipeManager() # Make this manager the default manager
You may override the get_queryset() method on your view to merge the private and public recipes for a user:
class RecipeViewSet(viewsets.ModelViewSet):
queryset = Recipe.objects.all()
serializer_class = RecipeSerializer
permission_classes = (AllowAny,)
def get_queryset(self):
if self.request.user:
private_recipes = Recipe.objects.private_recipes(user=self.request.user)
else:
private_recipes = Recipe.objects.none()
public_recipes = Recipe.objects.public_recipes()
final_recipes_list = private_recipes | public_recipes # Shorthand to merge two querysets
return final_recipes_list
I would actually recommend considering having different ViewSet's for public and private recipes, i.e.
class PublicRecipeViewSet(viewsets.ModelViewSet):
queryset = Recipe.objects.public_recipes()
class PrivateRecipeViewSet(viewsets.ModelViewSet):
queryset = Recipe.objects.filter(private=True)
def get_queryset(self):
if not self.request.user:
raise AuthenticationFailed()
queryset = super().get_queryset()
return queryset.filter(user=self.request.user)

Calling 'is_valid()' removes data from the serializer

When I am calling is_valid on my serializer, some of the data passed to the serializer is not getting saved. The files field is available in the serializer.initial_data, but does not get saved in serializer.validated_data. Any ideas?
Serializer:
class SomeSerializer(serializers.Serializer):
email = serializers.EmailField()
files = serializers.ListField(
child=serializers.FileField()
)
And the following view:
class SomeView(mixins.CreateModelMixin, generics.GenericAPIView):
permission_classes = (permissions.IsAuthenticated,)
serializer_class = SomeSerializer
def post(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
email = serializer.validated_data["email"]
files = serializer.validated_data.get("files")
#Do something here
return response

Categories