I am trying to make an api backend of something like reddit. I want to ensure that whoever is creating a post (model Post) within a particular subreddit is a member of that subreddit (subreddit model is Sub). Here is my latest effort, which works but seems pretty sloppy, and the serializer for some context.
Post permissions.py
class IsMemberOfSubOrReadOnly(BasePermission):
def has_permission(self, request, view):
if request.method in permissions.SAFE_METHODS:
return True
elif request.data:
# prevent creation unless user is member of the sub
post_sub_pk = get_pk_from_link(request.data['sub'])
user = request.user
user_sub_pks = [sub.pk for sub in user.subs.all()]
if not (post_sub_pk in user_sub_pks):
return False
return True
Post serializers.py
from .models import Post
from redditors.models import User
from subs.models import Sub
class PostSerializer(serializers.HyperlinkedModelSerializer):
poster = serializers.HyperlinkedRelatedField(
view_name='user-detail',
#queryset=User.objects.all(),
read_only=True
)
sub = serializers.HyperlinkedRelatedField(
view_name='sub-detail',
queryset=Sub.objects.all()
)
class Meta:
model = Post
fields = ('url', 'id', 'created', 'updated', 'title', 'body',
'upvotes', 'sub', 'poster')
The issue with this approach is that since 'sub' is a hyperlinkedRelatedField on the Post serializer what I get back from request.data['sub'] is just the string hyperlink url. I then have a function, get_pk_from_link that uses regex to read the pk off the end of the url. Then I can use that to grab the actual model I want and check things. It would be nice if there were a more direct way to access the Sub model that is involved in the request.
I have tried searching the fields of the arguments that are available and I can't find a way to get to the Sub object directly. Is there a way to access the Sub model object through the hyperlink url?
I have also solved this problem by just using a serializer field validator (not shown above), but I am interested to know how to do it this way too. Maybe this is just a bad idea and if so please let me know why.
You are right, parsing the url is not the way to go. Since you want to perform the permission check before creating a Post object, I suspect you cannot use object level permissions either, because DRF does not call get_object in a CreateAPIView (since the object does not exist in the database yet).
Considering this is a "business logic" check, a simpler approach would be to not have that permission class at all and perform the check in your perform_create hook in your view (I had asked a similar question about this earlier):
from rest_framework.exceptions import PermissionDenied
# assuming you have a view class like this one for creating Post objects
class PostList(generics.CreateApiView):
# ... other view stuff
def perform_create(self, serializer):
sub = serializer.get('sub') # serializer is already validated so the sub object exists
if not self.request.user.subs.filter(pk=sub.pk).exists():
raise PermissionDenied(detail='Sorry, you are not a member of this sub.')
serializer.save()
This saves you hassle of having to perform that url parsing as the serializer should give you the Sub object directly.
Related
BACKGROUND
I have two serializers: PostSerializer and PostImageSerializer which both inherit DRF ModelSerializer. The PostImage model is linked with Post by related_name='photos'.
Since I want the serializer to perform update, PostSerializer overrides update() method from ModelSerializer as stated in official DRF doc.
class PostSerializer(serializers.ModelSerializer):
photos = PostImageSerializer(many=True)
class Meta:
model = Post
fields = ('title', 'content')
def update(self, instance, validated_data):
photos_data = validated_data.pop('photos')
for photo in photos_data:
PostImage.objects.create(post=instance, image=photo)
return super(PostSerializer, self).update(instance, validated_data)
class PostImageSerializer(serializer.ModelSerializer):
class Meta:
model = PostImage
fields = ('image', 'post')
I have also defined a ViewSet which inherits ModelViewSet.
class PostViewSet(viewsets.ModelViewSet):
queryset = Post.objects.all()
serializer_class = PostSerializer
Finally the PostViewSet is registered to DefaultRouter. (Omitted code)
Goal
The goals are simple.
Send PUT request via PostMan with url like 'PUT http://localhost:8000/api/posts/1/'
Since Image files should be included, request would be done by form-data like below.
Problem
I'm getting 400 Response with error message as following.
{
"photos": [
"This field is required."
],
"title": [
"This field is required."
],
"content": [
"This field is required."
]
}
(Should you plz note that the error messages might not exactly fit with DRF error messages since they are translated.)
It is obvious that none of my PUT fields are applied.
So I have been digging around Django rest framework source code itself and found out serializer validation in ViewSet update() method continues to fail.
I doubt that because I PUT request not by JSON but by form-data using key-value pair so request.data is not properly validated.
However, I should contain multiple images in the request which means plain JSON would not work.
What would be the most clear solutions for this case?
Thank you.
Update
As Neil pointed out, I added print(self) at the first line of update() method of PostSerializer. However nothing printed out on my console.
I think this is due to my doupt above because perform_update() method which calls serializer update() method is called AFTER serializer is validated.
Therefore the main concept of my question could be narrowed to the followings.
How should I fix requested data fields so that validation inside update() method of ModelViewSet could pass?
Do I have to override update() method of ModelViewSet(not the one from ModelSerializer)?
Thanks again.
First of all you need to set header:
Content-Type: multipart/form-data;
But maybe if you set form-data in postman, this header should be
default.
You can't send images as a json data (unless you encode it to string and decode on server side to image eg. base64).
In DRF PUT by default requires all fields. If you want to set only partial fields you need to use PATCH.
To get around this and use PUT to update partial fields you have two options:
edit update method in viewset to partial update serializer
edit router to always call partial_update method in serializers which is more advanced
You can override viewset update method to always update serializer partial (changing only provided fields):
def update(self, request, *args, **kwargs):
partial = True # Here I change partial to True
instance = self.get_object()
serializer = self.get_serializer(instance, data=request.data, partial=partial)
serializer.is_valid(raise_exception=True)
self.perform_update(serializer)
return Response(serializer.data)
Add
rest_framework.parsers.MultiPartParser
to the main settings file to the REST_FRAMEWORK dict:
REST_FRAMEWORK = {
...
'DEFAULT_PARSER_CLASSES': (
'rest_framework.parsers.JSONParser',
'rest_framework.parsers.MultiPartParser',
)
}
Looking at your serializers it's weird that you don't get error from PostSerializer because you don't add "photos" field to Meta.fields tuple.
More advices from me in this case:
add required=False to your photos field (unless you want this to be required)
as wrote above add photos field to you Meta.fields tuple fields = ('title', 'content', 'photos',)
add default None value for your validated_data.pop('photos'), then check photos data is provided before loop.
The solution is somewhat a mixture or #Neil and #mon's answers. However I'll straighten out a bit more.
Analysis
Right now Postman submits form data which contains 2 key-value pairs(Please refer to the photo which I uploaded in my original question). One is "photos" key field linked with multiple photo files and the other is "data" key field linked with one big chunk of 'JSON-like string'. Although this is a fair method POSTing or PUTting data along with files, DRF MultiPartParser or JSONParser won't parse these properly.
The reason why I got the error message was simple. self.get_serializer(instance, data=request.data, partial=partial method inside ModelViewSet(especially UpdateModelMixin) couldn't understand request.data part.
Currently request.data from submitted form data looks like below.
<QueryDict: { "photos": [PhotoObject1, PhotoObject2, ... ],
"request": ["{'\n 'title': 'title test', \n 'content': 'content test'}",]
}>
Watch the "request" part carefully. The value is a plain string object.
However my PostSerializer expects the request.data to look something like below.
{ "photos": [{"image": ImageObject1, "post":1}, {"image": ImageObject2, "post":2}, ... ],
"title": "test title",
"content": "test content"
}
Therefore, let's do some experiment and PUT some data in accordance with above JSON form.
i.e
{ "photos": [{"image": "http://tny.im/gMU", "post": 1}],
"title" : "test title",
"content": "test content"
}
You'll get an error message as following.
"photos": [{"image": ["submitted data is not a file."]}]
Which means every data is submitted properly but the image url http://tny.im/gMU is not a file but string.
Now the reason of this whole problem became clear. It is the Parser which needs to be fixed so that the Serializer could understand submitted form data.
Solution
1. Write new parser
New parser should parse 'JSON-like' string to proper JSON data. I've borrowed the MultipartJSONParser from here.
What this parser does is simple. If we submit 'JSON-like' string with the key 'data', call json from rest_framework and parse it. After that, return the parsed JSON with requested files.
class MultipartJsonParser(parsers.MultiPartParser):
# https://stackoverflow.com/a/50514022/8897256
def parse(self, stream, media_type=None, parser_context=None):
result = super().parse(
stream,
media_type=media_type,
parser_context=parser_context
)
data = {}
data = json.loads(result.data["data"])
qdict = QueryDict('', mutable=True)
qdict.update(data)
return parsers.DataAndFiles(qdict, result.files)
2. Redesign Serializer
Official DRF doc suggests nested serializers to update or create related objects. However we have a significant drawback that InMemoryFileObject cannot be translated into a proper form which the serializer expects. To do this, we should
Override updatemethod of ModelViewSet
Pop out 'photos' key-value pair from request.data
Translate popped 'photos' pairs into the list of dictionaries containing 'image' and 'post' keys.
Append the result to request.data with a key name 'photos'. This is because our PostSerializer expects the key name to be 'photos'.
However basically request.data is a QuerySet which is immutable by default. And I am quite skeptical if we must force-mutate the QuerySet. Therefore, I'll rather commission PostImage creation process to update() method of the ModelViewSet. In this case, we don't need to define nested serializer anymore.
Simply just do this:
class PostSerializer(serializers.ModelSerializer):
class Meta:
model = Post
fields = '__all__'
class PostImageSerializer(serializer.ModelSerializer):
class Meta:
model = PostImage
fields = '__all__'
3. Override update() method from ModelViewSet
In order to utilize our Parser class, we need to explicitly designate it. We will consolidate PATCH and PUT behaviour, so set partial=True. As we saw earlier, Image files are carried with the key 'photos' so pop out the values and create each Photo instance.
Finally, thanks to our newly designed Parser, plain 'JSON-like' string would be transformed into regular JSON data. So just simply put eveything into serializer_class and perform_update.
class PostViewSet(viewsets.ModelViewSet):
queryset = Post.objects.all()
serializer_class = PostSerializer
# New Parser
parser_classes = (MultipartJsonParser,)
def update(self, request, *args, **kwargs):
# Unify PATCH and PUT
partial = True
instance = self.get_object()
# Create each PostImage
for photo in request.data.pop("photos"):
PostImage.objects.create(post=instance, image=photo)
serializer = self.get_serializer(instance, data=request.data, partial=partial)
serializer.is_valid(raise_exception=True)
# Do ViewSet work.
self.perform_update(serializer)
return Response(serializer.data)
Conclusion
The solution works, but I'm not sure this is the cleanest way of saving foreign key related models. I get a strong feeling that it is the serializer that should save the related model. Just as the doc stated, data other than files are saved that way. If somebody could tell me more subtle way to do this, I would be deeply appreciated.
In Django Rest Framework, if I have a retrieve view (or a detail_route or any other route that is meant to operate on a single item rather than a collection) then I have to have an argument called pk for the primary key or ID to be passed in.
For example:
def retrieve(self, request, pk=None):
...
return Response(data={'id': pk})
My question is what kind of validation (if any) is applied to pulling the PK from the URL? Also, is there an idiomatic way to validate it, rather than writing it inline in the view itself?
I have tried looking in the docs but so far haven't seen this covered.
In itself, using the pk should be pretty safe as it simply a str object. Apart from that you could use the DRF's serializer fields for validation. Lets say pk is a UUID. Calling .to_internal_value() on a field instance will validate given data and return a UUID object.
Nice thing here is that this will automatically throw a DRF ValidationError (in case of a invalid UUID) showing the same behaviour as if pk were part of a failed serializer validation.
from rest_framework import serializers
def retrieve(self, request, pk=None):
# throws ValidationError if pk is not a valid UUID
uuid = serializers.UUIDField().to_internal_value(data=pk)
return Response(data={'id': uuid})
Sidenote: you could also use Django's FormFields with the .clean() method to the same effect, but you would need to handle the thrown exception there yourself.
I have two questions. How i can just return true if relation exist? For example I have post model and comment model also, comment have foreginKey to post. Now after post serialization i wanna have something like this
{
id: 2
content: "My first post!"
has-comments: True
}
And my second question is how rename field name in model relation? Again we have post and comment. In comment model i have foregin key to post like
post = models.ForeignKey(Post)
Now when i add new comment i send JSON data with {post: postIdHere}. Is possible to change post to postId only in drf not in django model?
I hope you understand me :)
Best Redgards,
Sierran.
The closest thing I can come up with is a custom has_comments field (rather than has-comments) with this in the serializer:
from rest_framework import serializers
class YourSerializer(Either Serializer or ModelSerializer...):
has_comments = serializers.SerializerMethodField()
#staticmethod
def get_has_comments(instance):
# Choose whichever one works for you.
# You did not specify some model names, so I am just making stuff up.
return instance.post_set.exists()
return Comment.objects.filter(post_id=instance.id).exists()
You may also have to specify the field in the serializer's Meta class. When first run, the framework will tell you exactly how.
I am using Django REST as backend and angular as front end.
I have two serializers one for readong GET requests and other for POST, PUT requests.
This was because there are few fields like interval, etc which i am entering in integer by user but in the database i am saving as timedelta so i have to multitply them to make them as seconds on front end.
so e.g interval = 5 entered by user and i am posting 5*60*60 to server.
In order to read i have made ReadSerializer where i am diving that by 60*60 to again show to user what he added.
This is working find my problem is after saving my object to database the djnago rest frameework sends the object as it is saved which has interval = 5*60*60. inerval is just an example there 4-5 felds where i am changing them in front end before posting
Is there any way that response used my READ serializer before sending
class Serializer(serializers.ModelSerializer):
interval = serializers.SerializerMethodField('get_interval')
class Meta:
model = User
fields = ('id', 'interval')
def get_interval(self, obj):
return obj.interval/60*60
class WriteSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ('id', 'interval')
This is the view
class UserListCreateView(generics.ListCreateAPIView):
queryset = User.objects.filter(is_deleted=False)
def get_serializer_class(self):
if self.request.method == 'GET':
return ReadSerializer
return WriteSerializer
You can use the transform_field method which allows you to customize the representation of a field when showing it.
Documentation about it: http://www.django-rest-framework.org/api-guide/serializers/#customizing-field-representation
Btw, careful if you are planning to upgrade to new versions of drf (>=3), these methods may disappear in favour of a unique to_representation method: https://groups.google.com/forum/?fromgroups#!topic/django-rest-framework/9kp5kXCssR4
I would just change it in JavaScript on the front end.
However I have a similar issue and solved it like this, (warning, I just started using DRF, so may not be the best way):
Just change the response of the method directly by first getting the original response, then editing the data property of it, which is a dict (pre-serialized) of the data to return:
class UserListCreateView(generics.ListCreateAPIView):
...
def list(self, request, *args, **kwargs):
response = super().list(request, *args, **kwargs)
response.data['interval'] = response.data['interval'] / 60 / 60
return response
I wish to implement my new API with a nested resource.
Example: /api/users/:user_id/posts/
Will evaluate to all of the posts for a specific user. I haven't seen an working example for this use case, maybe this isn't the right way for implementing rest API?
As commented by Danilo, the #link decorator got removed in favor of #list_route and #detail_route decorators.
Update: #detail_route & #list_route got deprecated in favor of #action.
Here's the alternate solutions:
Solution 1:
#detail_route()
def posts(self, request, pk=None):
owner = self.get_object()
posts = Post.objects.filter(owner=owner)
context = {
'request': request
}
post_serializer = PostSerializer(posts, many=True, context=context)
return Response(post_serializer.data)
Solution 2:
Try drf-nested-routers. Haven't tried this out yet, but looks promising, many are already using it. Looks like an advanced version of what we are already trying to achieve.
Hope this helps.
To map /api/users/:user_id/posts/ you can decorate a posts method inside your ViewSet with #link()
from rest_framework.decorators import link
from rest_framework.response import Response
class UserViewSet(viewsets.ModelViewSet):
model = User
serializer_class = UserSerializer
# Your regular ModelViewSet things here
# Add a decorated method like this
#link()
def posts(self, request, pk):
# pk is the user_id in your example
posts = Post.objects.filter(owner=pk)
# Or, you can also do a related objects query, something like:
# user = self.get_object(pk)
# posts = user.post_set.all()
# Then just serialize and return it!
serializer = PostSerializer(posts)
return Response(serializer.data)
As commented by Danilo Cabello earlier you would use #detail_route or #list_route instead of #link(). Please read the documentation for "Routers", section "Extra link and actions" and "ViewSets", section "Marking extra actions for routing" for detailed explanations.