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

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
)

Related

In Django Rest Api, how do you return only the Items the owner uploaded

The Viewset def list looks like this:
class ThreeDimensionalModelViewSet(viewsets.ViewSet):
serializer_class = ThreeDimensionalModelSerializer
queryset = ThreeDimensionalModel.objects.all()
permission_classes = [permissions.IsAuthenticatedOrReadOnly]
def list(self, request):
models = ThreeDimensionalModel.objects.all()
serializer = ThreeDimensionalModelSerializer(models, many=True)
print(request.user.id)
return Response(serializer.data)
The serializer looks like this:
class ThreeDimensionalModelSerializer(serializers.ModelSerializer):
class Meta:
model = ThreeDimensionalModel
fields = ['File', 'Uploaded', 'Owner', 'Previous', 'SharedWithUser']
read_only_fields = ['Owner']
The model looks like this:
class ThreeDimensionalModel(models.Model):
File = models.FileField(upload_to='models')
Owner = models.ForeignKey('auth.User', on_delete=models.SET_NULL, null=True, related_name='Owner')
Uploaded = models.DateTimeField(auto_now_add=True)
Previous = models.ForeignKey("self", on_delete=models.SET_NULL, default=None, null=True)
SharedWithUser = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, related_name='SharedWithUser')
When a user requests models at /api/models it should only show the models that are the same owner Id as his.
If no additional data is sent with that request then obviously you can't filter by user.
The straightforward way to do it is that for logged in users the cookie will contain user information such as userId.
When your endpoint recognizes the user who made the requested is logged in, it will use that as the filter for the query instead of all() as seen in the Django docs
https://docs.djangoproject.com/en/3.2/topics/db/queries/#retrieving-specific-objects-with-filters
To summarize - if the user is not logged in (or supplies the information as part of the request in some way) then the request is anonymous and there is no way to know who made it

Is there a way to grab specific "fields" from request.data sent to the Django REST framework API in a POST method

I've got a Project model, with a project_code field. When the API receives a POST request, the request.data will also contain a project_code. I then want to filter my Project model objects based on the project_code inside the request.data
Once I've linked to request.data project_code to the Project model's project_code field, I want to save my Ticket model object to the database. Inside my Ticket model, there is a field called project which is related with a ForeignKey to the Project model.
Thus in essence the project_code inside the POST request.data needs to be used in order to save my Ticket model to the database with the correct Project model foreign Key.
Here are my models:
from django.db import models
class Project(models.Model):
project_code = models.TextField(blank=True)
project_description = models.TextField(blank=True)
def __str__(self):
return self.project_code
class Ticket(models.Model):
project = models.ForeignKey(Project, on_delete=models.CASCADE)
ticket_url = models.TextField(blank=True)
time_submitted = models.DateField(blank=True, auto_now_add=True)
description = models.TextField(blank=True)
user = models.TextField(blank=True)
type = models.TextField(blank=True)
def __str__(self):
return self.description
Here are my serializers:
from rest_framework import serializers
from ticketing_app_api.models import Ticket, Project
class TicketSerializer(serializers.ModelSerializer):
class Meta:
model = Ticket
fields = ['id', 'ticket_url', 'description', 'user', 'type']
And here are my views:
from ticketing_app_api.models import Ticket
from ticketing_app_api.serializers import TicketSerializer
from rest_framework import generics
from rest_framework.decorators import api_view
from rest_framework.response import Response
# from rest_framework.reverse import reverse
from rest_framework import status
#api_view(['GET', 'POST'])
def ticket_list(request):
"""
List all tickets, or creates a new ticket.
"""
if request.method == 'GET':
tickets = Ticket.objects.all()
serializer = TicketSerializer(tickets, many=True)
return Response(serializer.data)
elif request.method == 'POST':
serializer = TicketSerializer(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)
class TicketDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = Ticket.objects.all()
serializer_class = TicketSerializer
The cleaner approach would be to get the project_id when you create the project, and then just send it when creating the ticket. In this case, your TicketSerializer must also contain the project:
class TicketSerializer(serializers.ModelSerializer):
class Meta:
model = Ticket
fields = ["id", "ticket_url", "description", "user", "type", "project"]
and when you send the post request, you have to specify which is the project:
{
"ticket_url": "http://ticket.com",
"project": 1
}
In case you must use the project_code, you can set the project when validating the data:
class TicketSerializer(serializers.ModelSerializer):
class Meta:
model = Ticket
fields = ["id", "ticket_url", "description", "user", "type"]
def validate(self, attrs):
attrs["project"] = Project.objects.get(
project_code=self.initial_data.get("project_code")
)
return attrs

Django REST API endpoint for specific url

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

How to handle foreign keys with Django Rest Framework

I'm struggling to make my API work, the tutorials are quite tricky about this part. I want to have a '/comments/' POST request with body {movie_id: 1, content="Some comment") and connect it to some Movie.
In serializer I'm getting:
{'movie': [ErrorDetail(string='This field is required.', code='required')]}
How can I map movie_id to movie? By the way, I can change the name to movie if this would be easier.
Models.py:
from django.db import models
from django.utils import timezone
class Movie(models.Model):
title = models.CharField(max_length=200)
year = models.IntegerField()
class Comment(models.Model):
content = models.TextField(max_length=300)
publish_date = models.DateField(default=timezone.now())
movie = models.ForeignKey(Movie, on_delete=models.CASCADE, related_name='movie_id')
serializers.py:
class MovieSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Movie
fields = '__all__'
class CommentSerializer(serializers.HyperlinkedModelSerializer):
movie_id = serializers.PrimaryKeyRelatedField(many=False, read_only=True)
class Meta:
model = Comment
fields = '__all__'
views.py (for Comment, Movie works fine):
from .models import Movie, Comment
from rest_framework import viewsets, status
from rest_framework.response import Response
from .serializers import MovieSerializer, CommentSerializer
class CommentViewSet(viewsets.ModelViewSet):
queryset = Comment.objects.all()
serializer_class = CommentSerializer
def create(self, request, *args, **kwargs):
serializer = CommentSerializer(data=request.data, context={'request': request})
if serializer.is_valid(raise_exception=True):
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
else:
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
I think you can try like this:
class CommentSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Comment
fields = '__all__'
Also, related name is used for reverse relationship. So it will work like this:
If Comment Model has related_name comments like this:
class Comment(models.Model):
movie = models.ForeignKey(Movie, on_delete=models.CASCADE, related_name='comments')
Then you can access comments from movie like this:
for m in Movie.objects.all():
m.comments.all()
Nested data works a little differently to how I expected.
If you want to connect a comment to a movie, you need to pass the movie object to your comment, not the primary key of the movie object.
Under the hood, Django automatically creates a new field 'movie_id' on your comment object in which the movie's primary key is stored - but you don't need to worry about that. So I would call the field in the comment 'movie', otherwise Django will create a new field 'movie_id_id'.
I got something similar to work by defining a custom create method in my serializer:
In your serializer:
class CommentSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Comment
fields = '__all__'
def create(self, validated_data):
themovieid = validated_data.pop('movie_id', None) # remove movie_id from the comment data
themovie = Movie.objects.get(pk=themovieid) # find the movie object
return Comment.objects.create(movie=themovie, **validated_data)
I have tried to adapt this to your code, I hope it will help you to get this working. I have removed movie_id from your serializer: your model defiines everything that is needed.
Edit: have you tried simply passing the movie's id as 'movie' in your comment data, with no custom create method and do not define 'movie_id' in your serializer?

How can I filter a ManyToManyField against the current User in the Browsable API in DRF?

I have 2 models, Todo and a Tag. Todo has a ManyToMany relationship with Tag. When adding new Todos from the Browsable API, I want to be able to see only the Tags added by the current user as the available options in the multiselect. Currently, it shows all the added Tags, irrespective of who added them. I want to limit the options to only show the Tags added by the current user. (Authentication is setup already)
The models:
class Todo(models.Model):
title = models.CharField(max_length=100)
description = models.TextField(blank=True)
created_at = models.DateTimeField(auto_now_add=True)
due_at = models.DateTimeField(blank=True)
updated_at = models.DateTimeField(auto_now=True)
tags = models.ManyToManyField(Tag, related_name='todos')
creator = models.ForeignKey(User, on_delete=models.CASCADE, related_name='todos')
class Tag(models.Model):
name = models.CharField(max_length=20)
creator = models.ForeignKey(User, on_delete=models.CASCADE, related_name='created_tags')
def __str__(self):
return self.name
The Serializer:
class TodoCreateSerializer(serializers.ModelSerializer): #This is the one being used for a POST
class Meta:
model = models.Todo
fields = ('title', 'description', 'due_at', 'tags')
Is there some serializer field or some other way to specify which queryset to use in the Serializer? Is there another better approach?
In your TodoCreateSerializer you need to add PrimaryKeyRelatedField with a custom queryset that has the filtered tags of a user.
First, you will need to create a custom PrimaryKeyRelatedField that filter any objects to get only those who owned by the user.
class UserFilteredPrimaryKeyRelatedField(serializers.PrimaryKeyRelatedField):
def get_queryset(self):
request = self.context.get('request', None)
queryset = super(UserFilteredPrimaryKeyRelatedField, self).get_queryset()
if not request or not queryset:
return None
return queryset.filter(user=request.user)
(This is a generic one and can be used when filtering in objects by user)
Then you should use this one in you TodoCreateSerializer:
class TodoCreateSerializer(serializers.ModelSerializer):
tags = UserFilteredPrimaryKeyRelatedField(queryset= Tag.objects, many=True)
class Meta:
model = models.Todo
fields = ('title', 'description', 'due_at', 'tags')

Categories