Prevent user liking own comments in Django API - django

I'm hopeful someone can help with this problem. I believe the answer is probably straightfoward but it's eluding me. I'm creating a messaging API where users like comments, however, I want to prevent a user from liking their own comment
I have the following Like model linking to a Message model:
# models.py
class Like(models.Model):
message_id = models.ForeignKey(Message, related_name='message_id_like', on_delete=models.CASCADE)
owner = models.ForeignKey(User, on_delete=models.CASCADE)
like = models.BooleanField()
like_date = models.DateTimeField(auto_now_add=True)
def __str__(self):
return str(self.owner) + ', ' + self.message_id.message_title[:40]
With a serializer for the API
# serializers.py
class LikeSerializer(serializers.ModelSerializer):
owner = serializers.ReadOnlyField(source='owner.username')
class Meta:
model = Like
fields = [
'message_id',
'owner',
'like',
'like_date',
]
And a view:
# views.py
class LikeViewSet(viewsets.ModelViewSet):
queryset = Like.objects.all()
serializer_class = LikeSerializer
def perform_create(self, serializer): # Saving the user
serializer.save(owner=self.request.user)
I believe I need an additional function within the LikeViewSet class that prevents the creation of a like when Like user matches Message user, however, I do not know how to specify it.
Any help appreciated.

Do something like this. But preventing means a lot. Whether to show some error or silently prevent.
def perform_create(self, serializer): # Saving the user
if serializer.data['message_id'].user == self.request.user:
# error handle for self like
else:
serializer.save(owner=self.request.user)

Since you are using Django Rest Framework, the most elegant way is to write a custom Permission Class. It could look like this:
class IsNotOwnerCanLike(permissions.BasePermission):
def has_permission(self, request, view):
if view.action == "create":
return not request.data["message_id"].user == request.user
and then in your ViewSet you should specify
permission_classes = [IsNotOwnerCanLike]
This way, when the owner tries to like their own post, they will get a HTTP 403 Forbidden error.

Finally solved this using a variation of submitted answers. The secret sauce to avoid the serializer access error was the initial_data method (rather than the validated_data method which pulls back the wrong data).
def perform_create(self, serializer):
message = get_object_or_404(Message, pk=serializer.initial_data['message_id'])
if message.owner == self.request.user:
raise PermissionDenied
else:
serializer.save(owner=self.request.user)

Related

How to CreateAPIView using the request.user

Hi I'm wondering what the best practice is for creating a new model entry with a user based off the request in Django Rest Framework?
Models:
class Asset(models.Model):
user = models.ForeignKey(UserAccount, on_delete=models.CASCADE, related_name="assets")
name = models.CharField(max_length=200)
amount = models.DecimalField(max_digits=10, decimal_places=2)
Serializers:
class AssetSerializer(serializers.ModelSerializer):
class Meta:
model = Asset
fields = '__all__'
Views
class CreateAssetView(generics.CreateAPIView):
serializer_class = AssetSerializer
<Doesn't seem to work, possibly since user isn't in the json>
def perform_create(self, serializer):
serializer.save(user=self.request.user)
Basically I want to be able to send a POST request {name: 'myasset', amount: '50'} to this endpoint and have a new Asset saved with the User field obtain from the request. What is the best way to do this? Thanks
*** EDIT ***
Thought of a better solution:
class CreateAssetView(generics.CreateAPIView):
serializer_class = AssetSerializer
queryset = Asset.objects.all()
def perform_create(self, serializer):
serializer.save(user=self.request.user)
However this means I must send a dummy user_id in the POST request from the front-end. I'm not sure how this can be avoided. Any suggestions highly welcome.
I do most often this thing using function-based views not class-based ones. :
Basically, that will also be able to send a POST request and will save the user who requested the post request.
from rest_framework.response import Response
from rest_framework.decorators import api_view
from rest_framework.permissions import IsAuthenticated
#api_view(['POST'])
#permission_classes([IsAuthenticated])
def perform_create(request, pk):
user = request.user
asset= Asset.objects.get(id=pk)
data = request.data
# Create Asset
asset = Asset.objects.create(
user=user,
name=user.first_name,
amount=data['amount '],
)
asset.save()
return Response('Asset Added')
And to return the data I create another view for the serialized data
where needed. I guess there would be other approaches I'm sure but
this one is much simple and easy to do.
Since Post "author" cannot be null then we need to provide a user,
one way to do this is to put the user instance in the request.data from the frontend...
the example below is assigning the user instance to request.data from the backend after the request is made!
...models.py
class Post(models.Model):
author = models.ForeignKey(User, on_delete=models.CASCADE)
title = models.CharField(max_length=255)
content = models.CharField(max_length=255)
...views.py
class PostCreate(CreateAPIView):
queryset = Post
serializer_class = PostSerializer
# override this method
def create(self, request, *args, **kwargs):
request.data['author'] = request.user.pk
return super().create(request, *args, **kwargs)

Adding a user to a many to many field from user input

I am creating a functionality where a project/showcase model has an administrators ManyToManyField, which would contain a list of users that can control a project.
I am having issues adding users to this field (the user will be gotten from user input from the front end). I was able to set the person that creates a project as an administrator by default, but adding other administrators has not been successful.
models.py
class Showcase(models.Model):
title = models.CharField(max_length=50)
description = models.TextField(null=True)
skill_type = models.ForeignKey(Skill, on_delete=models.CASCADE)
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.DO_NOTHING, related_name="Showcases")
content = models.TextField(null=True)
created_on = models.DateTimeField(auto_now_add=True)
updated_on = models.DateTimeField(auto_now=True)
voters = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name="upvotes")
slug = models.SlugField(max_length=255, unique=True)
administrator = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name="administrators", blank=True)
serializers.py
class ShowcaseAdminSerializer(serializers.ModelSerializer):
class Meta:
model = Showcase
fields = ['administrator',]
With the view below I wanted only administrators to be able to add new administrators to the showcase. The user to be added will be gotten from the front end input instead of the URL (I hope this part is understood).
views.py
class showcaseAddAdminAPIView(APIView):
'''
Add a user as an admin to a showcase
'''
serializer_class = ShowcaseAdminSerializer
permission_classes = [IsAdmin]
def post(self, request, slug):
showcase = get_object_or_404(Showcase, slug=slug)
if request.user in showcase.administrator.all():
showcase.administrator.add(user.slug)
showcase.save()
serializer = self.serializer_class(showcase)
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
with the view above, I was wonder if the check for if the user is an administrator before he can make the action should also be done using permissions. I have permission below which is not working though.
urls.py
path("<slug:slug>/addadmin/", qv.showcaseAddAdminApiview.as_view(), name="add-administrator-to-showcase"),
permissions
class IsAdmin(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
if request.method in permissions.SAFE_METHODS:
return False
return obj.administrator == request.user
i tried using this permission to make suer only administrators can add admin to the showcase. but it doesnt work.
Your permission won't work because as per documentation:
Note that the generic views will check the appropriate object level permissions, but if you're writing your own custom views, you'll need to make sure you check the object level permission checks yourself. You can do so by calling self.check_object_permissions(request, obj) from the view once you have the object instance. This call will raise an appropriate APIException if any object-level permission checks fail, and will otherwise simply return.
So the view should look like this:
class showcaseAddAdminAPIView(APIView): # Use CamelCase for naming class
'''
Add a user as an admin to a showcase
'''
...
def post(self, request, slug):
showcase = get_object_or_404(Showcase, slug=slug)
self.check_object_permissions(request, showcase)
Update the permission class:
class IsAdmin(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
if request.method in permissions.SAFE_METHODS:
return False
return request.user.administrators.filter(pk=obj.pk).exists()
And update the view:
# rest of the code
def post(self, request, slug): # PUT is more suited for updating instance
showcase = get_object_or_404(Showcase, slug=slug)
try:
self.check_object_permissions(request, showcase)
serializer = self.serializer_class(showcase, data=request.data, partial=True)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
else:
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except APIException:
return Response(status=status.HTTP_403_FORBIDDEN)
Update
Override the update method in serializer:
class ShowcaseAdminSerializer(serializers.ModelSerializer):
def update(self, instance, validated_data):
users = validated_data.get('administrator')
for user in users:
instance.administrator.add(user)
return instance
# rest of the code
As Opeyemi said if request.user in showcase.administrator.all(): will never add new administrators, if the user making the request is not an administrator it will be denied and if the user making a request is already an administrator it wont add anything new. You need to pass in an user id and use it to add a new user while being an administrator, showcase.administrator.add(user.id)

How to deal with user_id field properly?

I'm working myself through a lot of examples, but I can't find a way that works 100%.
class QuestionViewSet(viewsets.ModelViewSet):
queryset = QNAQuestion.objects.all()
serializer_class = QuestionSerializer
permission_classes = (IsOwnerOrReadOnly, )
filter_fields = ('id', 'user')
filter_backends = (filters.DjangoFilterBackend, filters.OrderingFilter)
def perform_create(self, serializer):
serializer.save(user=self.request.user)
This so far works fine, but it still requires the user_id to be given by the user input even though it's ignored and relpaced by request.user.
class QuestionSerializer(serializers.ModelSerializer):
class Meta:
user = serializers.ReadOnlyField()
model = QNAQuestion
fields = ('id','user','subject', 'body', 'solution')
So I think I have to modify my serializer. I tried HiddenInput and ReadOnly, but both don't really do the trick. If I make it hidden, than the the user_id is not required anymore, but it's also hidden when looking at existing objects. If I make it read only it's not required, but saving the serializer doesn't work anymore. I get the error message that the django object is not serializable to JSON.
class IsOwnerOrReadOnly(permissions.BasePermission):
"""
Object-level permission to only allow owners of an object to edit it.
Assumes the model instance has an `user` attribute.
"""
def has_object_permission(self, request, view, obj):
# Read permissions are allowed to any request,
# so we'll always allow GET, HEAD or OPTIONS requests.
if not request.user.is_authenticated():
return False
if request.method in permissions.SAFE_METHODS:
return True
# Instance must have an attribute named `owner`.
return obj.user == request.user
So how can I get it fixed? The user_id should be visible, but I want it to be request.user and i don't want it to be required on creating new objects. Ideally it should also be hidden when using the auto generated api gui.
Thank you for your time. Sorry for spelling or grammar mistakes, I'm not a native speaker.
Try to make the field only required=False instead of Hidden or ReadOnly.
class QuestionSerializer(serializers.ModelSerializer):
class Meta:
model = QNAQuestion
fields = ('id','user','subject', 'body', 'solution')
extra_kwargs = {
'user': {'required': False}
}

How to set different permissions for different methods such as POST and List when use Django REST Framework's ListCreateAPIView

I built a Follow model to record the social networking behaviour and would like to simulate the following action. Every authenticated user can follow the others.
class Follow(models.Model):
user = models.ForeignKey(User)
follower = models.ForeignKey(User, related_name="followers")
follow_time = models.DateTimeField(auto_now_add=True, blank=True)
class Meta:
unique_together = ('user', 'follower')
def __unicode__(self):
return u'%s, %s' % (self.user.username, self.follower)
And the FollowSerializer is:
class FollowSerializer(serializers.ModelSerializer):
class Meta:
model = Follow
field = ('user', 'follower', 'follow_time')
The view that I am using is:
class FollowingEnumByUserID(generics.ListCreateAPIView):
serializer_class = FollowSerializer
def get_queryset(self):
follower_id = self.kwargs['pk']
return Follow.objects.filter(follower=follower_id)
I am registering it in the urls as:
url(r'^api/users/(?P<pk>[0-9]+)/following/$', views.FollowingEnumByUserID.as_view()),
Every authenticated user can view the following relation, no restricts. But I would like to just allow the authenticated user to add the following relation by himself/herself, which means there should be request.user == follower. How can I do this?
I would like to add the FollowingDelete view to just allow the user to add a following relation by himself/herself.
So I updated the url.py as:
url(r'^api/users/(?P<pk>[0-9]+)/following/$', views.FollowingEnumByUserID.as_view()),
url(r'^api/users/(?P<pk>[0-9]+)/following/(?P<following_id>[0-9]+)/$', views.FollowingDelete.as_view()),
The permission that I am using is:
class IsFollowerOrReadOnly(permissions.BasePermission):
"""
View-level permission to allow the follower to edit the following relation
"""
def has_permission(self, request, view):
if request.method in permissions.SAFE_METHODS:
return True
try:
follower = User.objects.get(id=view.kwargs["pk"])
except User.DoesNotExist:
#Reject any request for an invalid user
return False
return follower == request.user
And the views are:
class FollowingEnumByUserID(generics.ListCreateAPIView):
serializer_class = FollowSerializer
permission_class = (IsFollowerOrReadOnly)
def get_queryset(self):
"""
List all the people the input user is following
"""
follower_id = self.kwargs['pk']
return Follow.objects.filter(follower=follower_id)
class FollowingDelete(generics.DestroyAPIView):
serializer_class = FollowSerializer
permission_class = (IsAuthenticated, IsFollowerOrReadOnly)
def get_queryset(self):
user_id = self.kwargs['following_id']
follower_id = self.kwargs['pk']
return Follow.objects.filter(user=user_id, follower=follower_id)
Now the questions are:
The permission class doesn't work totally.
How to rewrite the DestroyAPIView, should I override the get_queryset function?
Django REST framework provides custom permission handling that allows you to handle complex permissions on the view and object level. In order to do what you are looking for, you are going to have to create a custom permission, but it's surprisingly easy.
Every authenticated user can follow the others.
DRF provides an IsAuthenticated permission that allows you to do this very easily. All you have to do is add it to the permission_classes on the view, or globally through the settings.
from rest_framework import permissions
class FollowingEnumByUserID(generics.ListCreateAPIView):
serializer_class = FollowSerializer
permission_classes = (permissions.IsAuthenticated, )
def get_queryset(self):
follower_id = self.kwargs['pk']
return Follow.objects.filter(follower=follower_id)
There is another restriction, which is the one that requires the custom permission class.
But I would like to just allow the authenticated user to add the following relation by himself/herself
This requires checking the request method (which I'm assuming is POST) and also the user who is being followed.
Lets start off with the easy check, the request method. Django REST framework provides permission classes that check the request method, such as IsAuthenticatedOrReadOnly, so we can look at the code to see how it is being done. From there it's just a matter of having a check against the request type.
class PostIfFollower(BasePermission):
"""
The request is not POST or the request user is the follower.
"""
def has_permission(self, request, view):
if request.method != "POST":
return True
return False
This code will reject all requests that come in using the POST method, while allowing all others. The second step in creating this permission is doing the user check, so only the follower can add new people that they are following. This requires getting the follower and checking that against request.user.
class PostIfFollower(BasePermission):
"""
The request is not POST or the request user is the follower.
"""
def has_permission(self, request, view):
if request.method != "POST":
return True
try:
follower = User.objects.get(id=view.kwargs["pk"])
except User.DoesNotExist:
# Reject any requests for an invalid user
return False
return follower == request.user
This builds upon the last permission class by getting the user from the url (not allowing it if the user doesn't exist) and checking if they are the current user.
After I modified the typos, the permission class works now:
class FollowingEnumByUserID(generics.ListCreateAPIView):
serializer_class = FollowSerializer
permission_classes = (IsFollowerOrReadOnly,)
def get_queryset(self):
"""
List all the people the input user is following
"""
follower_id = self.kwargs['pk']
return Follow.objects.filter(follower=follower_id)
class FollowingDelete(generics.DestroyAPIView):
serializer_class = FollowSerializer
permission_classes = (IsAuthenticated, IsFollowerOrReadOnly,)
def get_queryset(self):
user_id = self.kwargs['following_id']
follower_id = self.kwargs['pk']
return Follow.objects.filter(user=user_id, follower=follower_id)
And I make the FollowingDelete view work successfully by overriding the get_object() function.
def get_object(self, *args, **kwargs):
user_id = self.kwargs['following_id']
follower_id = self.kwargs['pk']
try:
return Follow.objects.get(user=user_id, follower=follower_id)
except Follow.DoesNotExist:
raise Http404("No such following relation")

Django TastyPie PUT Nested User Model Correctly

I'm trying to go with-the-grain using Django TastyPie to update my models. I have an Identity model, acting as a wrapper around default Django user model:
class Identity(ProfileBase):
user = models.OneToOneField(User, related_name='identity')
avatar = models.ImageField(upload_to=avatar_upload_path, blank=True,
null=True)
I have my UserResource:
class UserResource(ModelResource):
class Meta:
resource_name = 'user'
queryset = User.objects.all()
fields = ['email', 'first_name', 'last_name']
include_resource_uri = False
And I have my IdentityResource:
class IdentityResource(ModelResource):
user = fields.ToOneField(UserResource, 'user', full=True)
class Meta:
resource_name = 'identity'
queryset = Identity.objects.select_related()
fields = ['user', 'avatar']
always_return_data = True
include_resource_uri = False
authentication = OAuthTokenAuthentication()
authorization = Authorization()
I'm currently successfully updating first_name, last_name using the ModelResource obj_update method within IdentityResource:
def obj_update(self, bundle, request, **kwargs):
print 'updating object'
bundle = self.full_hydrate(bundle)
bundle.obj.user = request.user
user = bundle.data['user']
bundle.obj.user.first_name = user['first_name']
bundle.obj.user.last_name = user['last_name']
return super(IdentityResource, self).obj_update(bundle, request, user=request.user)
I want to make a PUT request and optionally update any field on the user or identity models (first_name, last_name on user, or the avatar field on identity). I would rather not have to manually access each field from the bundle data and set them on models manually, as I have done above.
How can I do this naturally in TastyPie? Can someone explain a better approach to solving this problem? Any direction is GREATLY appreciated. :)
Here's my shot at providing an answer that attempts to leverage Tastypie as much as possible.
It is a little more generic than the OP's request (it will update any user, not just the one logged in). In the real world you would probably want to add some sort of authentication/authorization.
from tastypie.resources import ModelResource
from tastypie.authorization import Authorization
from django.contrib.auth.models import User
from myapp.account.models import Identity
class IdentityResource(ModelResource):
class Meta:
queryset = Identity.objects.all()
class UserResource(ModelResource):
class Meta:
queryset = User.objects.all()
allowed_list_methods = ['get']
allowed_detail_methods = ['get','put']
authorization = Authorization()
def dehydrate(self, bundle):
identity_bundle = self.build_identity_bundle(bundle)
identity_bundle = IdentityResource().full_dehydrate(identity_bundle)
return identity_bundle
def obj_update(self, bundle, request, **kwargs):
user_bundle = super(UserResource, self).obj_update(bundle, request, **kwargs)
identity_bundle = self.build_identity_bundle(user_bundle)
IdentityResource().obj_update(identity_bundle, request)
return user_bundle
def build_identity_bundle(self, user_bundle):
identity_bundle = IdentityResource().build_bundle(
obj=user_bundle.obj.get_profile(),
data=user_bundle.data
)
return identity_bundle
What the example supports is:
GET a flattened User+Identity resource
PUT a flattened User+Identity resource, updating both models
You would want to register the UserResource in the API, and probably not the IdentityResource.
You could do something like this.
# Find all properties in user model.
properties = [prop for prop in bunder.obj.user if not prop.startswith('__')]
bundle_user = bundle.data['user']
# Find the property in bundle user and set it back on user if it exists.
for property in properties:
if property in bundle_user:
setattr(bundle.obj.user, property, bundle_user[property])
Maybe I'm missing the point but did you try a PATCH-method request? Tastypie will take all the sent attributes and update them in the database leaving all not-send attributes untouched.