Django REST API endpoint for specific url - python

Im trying to create an endpoint for a post and its comments in the following format:
/posts (view all posts)
/posts/{id} (view post by id)
/posts/{id}/comments (view comments for a post)
The first 2 work, but for the last one I have /comments rather than the url i would like and I am not sure how to go about that, I think I need to change my models for it.
My current models (its using default Django User):
class Post(models.Model):
author = models.ForeignKey(User, on_delete=models.CASCADE)
title = models.CharField(max_length=255)
description = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return self.title
class PostComment(models.Model):
author = models.ForeignKey(User, on_delete=models.CASCADE)
post = models.ForeignKey(Post, on_delete=models.CASCADE)
comment = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return self.post.title
And urls:
router = routers.DefaultRouter()
router.register(r'users', views.UserViewSet)
router.register(r'posts', views.PostViewSet)
router.register(r'comments', views.PostCommentViewSet)
Edit: this are the viewsets
class UserViewSet(viewsets.ModelViewSet):
queryset = User.objects.all().order_by('id')
serializer_class = UserSerializer
class PostViewSet(viewsets.ModelViewSet):
queryset = Post.objects.all().order_by('created_at')
serializer_class = PostSerializer
class PostCommentViewSet(viewsets.ModelViewSet):
queryset = PostComment.objects.all().order_by('created_at')
serializer_class = PostCommentSerializer

You can achieve this by writing the custom viewset actions--(drf doc),
from rest_framework.decorators import action
from rest_framework.response import Response
class PostViewSet(viewsets.ModelViewSet):
queryset = Post.objects.all().order_by('created_at')
serializer_class = PostSerializer
#action(detail=True, url_path='comments', url_name='post-comments')
def comments(self, request, *args, **kwargs):
queryset = PostComment.objects.filter(post=kwargs['pk'])
serializer = PostCommentSerializer(queryset, many=True, context= {'request':request, 'view':self})
return Response(data=serializer.data)

Your view should be something similar to this -
class PostCommentViewSet(viewsets.ModelViewSet):
queryset = PostComment.objects.all().order_by('created_at')
serializer_class = PostCommentSerializer
#action(detail=True)
def comments(self, request, id=None):
....
You can refer to DRF documentation for more detail here - https://www.django-rest-framework.org/api-guide/routers/#routing-for-extra-actions

If you want to use the router, then this is probably achievable by implementing a custom router, like in this example: https://www.django-rest-framework.org/api-guide/routers/#example

I think you forgot to register the viewset route parameters with the action decorator
https://www.django-rest-framework.org/api-guide/viewsets/#marking-extra-actions-for-routing
it should work if you had in your viewset
from rest_framework.decorators import action
#actions(detail=True)
def comments(self, request, pk):
# things to do here

Related

Creating a DetailView of a profile with a queryset of posts created by that user

I'm creating a twitter-like app and I'm stuck on creating a UserProfileView which is supposed to display a certain User's profile, along with a list of posts made by that user below. Though I can't really figure out a way to create a proper view for that.
I'm trying to use class based views for that, the one I'll be inheriting from is probably DetailView (for profile model) and something inside of that which retrieves a queryset of posts made by that user:
My profile model looks like this:
class Profile(models.Model):
user = models.OneToOneField(
User, on_delete=models.CASCADE, primary_key=True)
display_name = models.CharField(max_length=32)
profile_picture = models.ImageField(
default='assets/default.jpg', upload_to='profile_pictures')
slug = models.SlugField(max_length=150, default=user)
def get_absolute_url(self):
return reverse("profile", kwargs={"pk": self.pk})
Post model:
class Post(models.Model):
author = models.ForeignKey(User, on_delete=models.CASCADE)
date_posted = models.DateField(auto_now_add=True)
content = models.TextField(max_length=280)
image = models.FileField(upload_to='post_images/', blank=True, null=True)
def __str__(self) -> str:
return f'Post by {self.author} on {self.date_posted} - {self.content[0:21]}'
def get_absolute_url(self):
return reverse("post-detail", kwargs={"pk": self.pk})
I've tried creating this method:
class UserProfileView(DetailView):
model = Profile
context_object_name = 'profile'
template_name = 'users/profile.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['user_posts'] = Post.objects.filter(author=Profile.user)
return context
But this one sadly doesn't work, raising an error of
"TypeError: Field 'id' expected a number but got <django.db.models.fields.related_descriptors.ForwardOneToOneDescriptor object at 0x000001A5ACE80250>."
'ForwardOneToOneDescriptor' object has no attribute 'id' is returned if I replace the filter argument with author=Profile.user.id
I'm not sure whether it's a problem with the way I filtered Posts, or how I used get_context_data.
The object is stored as self.object, so you can filter with:
class UserProfileView(DetailView):
model = Profile
context_object_name = 'profile'
template_name = 'users/profile.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['user_posts'] = Post.objects.filter(author_id=self.object.user_id)
return context
An alternative might be to use a ListView for the Posts instead, to make use of Django's pagination:
from django.shortcuts import get_object_or_404
from django.views.generic import ListView
class UserProfileView(ListView):
model = Post
context_object_name = 'posts'
template_name = 'users/profile.html'
paginate_by = 10
def get_queryset(self, *args, **kwargs):
return (
super()
.get_queryset(*args, **kwargs)
.filter(author__profile__slug=self.kwargs['slug'])
)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['profile'] = get_object_or_404(Profile, slug=self.kwargs['slug'])
return context
Note: It is normally better to make use of the settings.AUTH_USER_MODELĀ [Django-doc] to refer to the user model, than to use the User modelĀ [Django-doc] directly. For more information you can see the referencing the User model section of the documentation.

validate method not called django rest framework

I am trying to test validate method on modelSerializer but it is NOT CALLED.
Why is not working ? Have i been missed something ? the same scenario works at different project
at urls
urlpatterns = [ path('api/allposts/', allposts, name='allposts') ]
at views:
from .serializer import PostSerializer
from rest_framework.renderers import JSONRenderer
from .models import Post
from django.http import JsonResponse
import json
def allposts(request):
qs = Post.objects.all()[:3]
ser = PostSerializer(qs, many=True)
data = JSONRenderer().render(ser.data)
return JsonResponse(json.loads(data), safe=False)
at models
class Post(models.Model):
title = models.CharField(max_length=100)
url = models.URLField()
poster = models.ForeignKey(User, on_delete=models.CASCADE, related_name='posts')
created = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ['-created']
def __str__(self):
return self.title
at serializer
from rest_framework import serializers
from .models import Post
class PostSerializer(serializers.ModelSerializer):
class Meta:
model = Post
fields = ['title', 'poster', 'url']
def validate(self, data):
if 'facebook' in data.get('url'):
raise serializers.ValidationError('you can not add facebook')
return data
serializer's validate method is called when you call serializer.is_valid(). Since you are serializing db instances you are not required to call is_valid(), hence validate() method is not called

different queryset based on permissions in Django Rest Framework

I have seen this link, but I didn't find anything related to my question helping it being resolved.
Imagine we have to create a blog, in which posts have two status:
is_draft
published (published == !is_draft)
So, each user should see all of his/her posts, whether it is draft or not. In addition, Other users should see the published posts of rest of the users.
I am using viewsets in django and I know that we should have different queryset based on the current user permissions but I don't know how.
models.py:
from django.db import models
# Create your models here.
from apps.authors.models import Author
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
)
views.py:
from django.shortcuts import render
from rest_framework import viewsets, permissions
# Create your views here.
from apps.posts.models import Post
from apps.posts.serializers import PostSerializer
class PostViewSet(viewsets.ModelViewSet):
queryset = Post.objects.all()
serializer_class = PostSerializer
def get_permissions(self):
if self.action == "create":
self.permission_classes = [permissions.IsAuthenticated]
elif self.action == "list":
pass #I don't know how can I change this part
return super(PostViewSet, self).get_permissions()
serializers.py:
from rest_framework import serializers
from apps.posts.models import Post
class PostSerializer(serializers.ModelSerializer):
class Meta:
model = Post
fields = '__all__'
Change your queryset like this in your viewset. That way, only your desired posts will be accessed/permitted by the view:
from django.shortcuts import render
from django.db.models import Q
from rest_framework import viewsets, permissions
# Create your views here.
from apps.posts.models import Post
from apps.posts.serializers import PostSerializer
class PostViewSet(viewsets.ModelViewSet):
serializer_class = PostSerializer
def get_permissions(self):
if self.action == "create":
self.permission_classes = [permissions.IsAuthenticated]
return super(PostViewSet, self).get_permissions()
def get_queryset(self, *args, **kwargs):
current_user = self.request.user
current_author = Author.objects.get(user=current_user) #assuming your author class has foreign key to user
return Post.objects.filter(Q(author=current_author) | Q(is_draft=False))

How to use a Many-to-Many through model in DRF

So I'm trying to achieve the general "Like" functionality in a social media website using Django and REST Framework, and a frontend in React.
Using a Post model to save all the posts, and I have a Many-to-Many field for storing the likes and created a through model as follows:
class PostLike(models.Model):
user = models.ForeignKey(AppUser, on_delete=models.CASCADE)
post = models.ForeignKey("Post", on_delete=models.CASCADE)
timestamp = models.DateTimeField(auto_now_add=True)
class Post(models.Model):
user = models.ForeignKey(AppUser, on_delete=models.CASCADE)
caption = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
edited_at = models.DateTimeField(auto_now=True)
likes = models.ManyToManyField(
AppUser, related_name="post_user", blank=True, through=PostLike
)
(AppUser is a custom auth model used)
Similarly, I have created serializers and viewsets for the above models:
class PostSerializer(serializers.ModelSerializer):
class Meta:
model = Post
fields = "__all__"
class PostLikeSerializer(serializers.ModelSerializer):
class Meta:
model = PostLike
fields = "__all__"
class PostViewSet(viewsets.ModelViewSet):
queryset = Post.objects.all()
serializer_class = PostSerializer
class PostLikeViewSet(viewsets.ModelViewSet):
queryset = PostLike.objects.all()
serializer_class = PostLikeSerializer
My question is, how do I "like" or remove an existing "like" from a post using API calls?
One method I know is to just make a POST request to the PostLike endpoint using the user PK and the post PK to create a PostLike instance, but I don't know a way to "remove" a like using the same method.
Please help!
you can use APIView instead of ViewSet like this:
from rest_framework import views
class PostLikeApiView(views.APIView):
serializer = PostLikeSerializer(data=request.data)
if serializer.is_valid():
user = serializer.data['user']
post = serializer.data['post']
post_like_obj = PostLike.objects.filter(user=user, post=post)
if post_like_obj.exists():
post_like_obj.delete()
result = 'unliked'
else:
PostLike.objects.create(user=user, post=post)
result = 'liked'
return Response(
{
'result': result,
},
status=status.HTTP_200_OK
)
else:
return Response(
serializer.errors,
status=status.HTTP_400_BAD_REQUEST
)

Does ViewSets generate view_names in Django Rest Framework?

I want to use HyperlinkedIdentityField in my serializer, that's why I need view name. How can I get the view name if I need model_name-list for example?
Here is my serializer:
class CategorySerializer(serializers.HyperlinkedModelSerializer):
posts = serializers.HyperlinkedIdentityField(view_name='', format=None)
class Meta:
model = Category
fields = ('url', 'category_name', 'id', 'parent', 'posts')
This is my views.py:
class PostViewSet(viewsets.ModelViewSet):
queryset = Post.objects.all()
serializer_class = PostSerializer
And here is my urls.py:
router = routers.DefaultRouter()
router.register(r'categories', views.CategoryViewSet)
router.register(r'posts', views.PostViewSet)
urlpatterns = [
url(r'^', include(router.urls)),
]
My models:
class Post(models.Model):
post_name = models.CharField(max_length=50)
post_text = models.CharField(max_length=1000)
pub_date = models.DateTimeField('date published')
categories = models.ManyToManyField('Category', related_name='posts')
def __str__(self):
return self.post_name
class Category(models.Model):
category_name = models.CharField(max_length=50)
parent = models.ForeignKey('self', null=True, blank=True, on_delete = models.CASCADE)
def __str__(self):
return self.category_name
I need a view_name for post-list, in order to use HyperlinkedIdentityField
As the Documentation for routers in DRF (http://www.django-rest-framework.org/api-guide/routers/) says, if the base_name argument is not passed when registering the ViewSet then the view_name is auto generated depending on the queryset attribute of the ViewSet registered while defining the router.
In your case the view_name should be 'post-list' for the list view and 'post-detail' for the detail view of the viewset unless you want to change/override it by passing it as the third parameter to router.register().
Also note that the queryset attribute should always be present if you are not initializing the base_name for the ViewSet which otherwise will throw an error.

Categories