When I try to use Django Rest Framework extra actions on a viewset, I can not make the decorator's serializer_class work.
class ClientViewSet(ModelViewSet):
queryset = Client.objects.all()
serializer_class = ClientSerializer
def get_queryset(self):
# Do things
def get_serializer_class(self):
if self.action in ["create"]:
return CreateClientSerializer
elif self.action in ["retrieve"]:
return ClientDetailSerializer
return self.serializer_class
#action(detail=True, methods=["get"], serializer_class=ClientDetailSerializer)
def get_by_name(self, request, name=None):
"""
Get one Client searching by name.
#param request:
#param name: Client code
#return: Response
"""
queryset = get_object_or_404(Client, name__iexact=name)
serializer = self.get_serializer(queryset)
return Response(serializer.data)
So, even if the extra action is supposedly overriding the ViewSet default serializer class, I still get ClientSerializer instead of ClientDetailSerializer.
The official documentation states that...
The decorator allows you to override any viewset-level configuration such as permission_classes, serializer_class, filter_backends...:
My get_serializer_class override defaults to the ViewSet serializer_class attribute for my extra actions. If I understand correctly, this is basically what GenericAPIView get_serializer_class does under the hood:
def get_serializer_class(self):
"""
(...)
"""
assert self.serializer_class is not None, (
"'%s' should either include a `serializer_class` attribute, "
"or override the `get_serializer_class()` method."
% self.__class__.__name__
)
return self.serializer_class
I guess I'm missing something obvious here. Just can not figure out what...
Any help is appreciated. Thanks in advance :)
Why not use it like this? I'm guessing you're doing something wrong in get_serializer_class.
#action(detail=True, methods=["get"], serializer_class=ClientDetailSerializer)
def get_by_name(self, request, name=None):
"""
Get one Client searching by name.
#param request:
#param name: Client code
#return: Response
"""
object = get_object_or_404(Client, name__iexact=name)
serializer = ClientDetailSerializer(object)
return Response(serializer.data)
When you override the get_serializer_class without calling the super of this class, the super class doesn't run.
user this:
def get_serializer_class(self):
if self.action in ["create"]:
return CreateClientSerializer
elif self.action in ["retrieve"]:
return ClientDetailSerializer
return super().get_serializer_class()
Related
I've got myself into a trouble with UniqueTogetherValidator.
The problem is that ReviewSerliazer unlike CommentSerializer which is almost identical does unique together validation before actually getting a title value from kwargs and sends back 400 answer with title field being required. I've tried to identify it as a HiddenField, but although serializer validation goes fine within tests, model validation does not. And I receive django.db.utils.IntegrityError: UNIQUE constraint failed: reviews_review.author_id, reviews_review.title_id
Key idea is that title should not be included in serializer response. Making it PrimaryKeyRelatedField and then doing the to_representation seems odd to me.
Any ideas how to fix this without catching exceptions in the viewset, which is obviously wrong?
models.py
class BasePost(models.Model):
text = models.TextField()
author = models.ForeignKey(
User, on_delete=models.CASCADE, related_name='%(class)ss')
pub_date = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ('-pub_date', )
abstract = True
def __str__(self):
return self.text[:30]
class Review(BasePost):
score = models.IntegerField(
default=0, validators=[MaxValueValidator(10), MinValueValidator(1)])
title = models.ForeignKey(
Title, on_delete=models.CASCADE, related_name='reviews')
class Meta(BasePost.Meta):
constraints = [
models.UniqueConstraint(
fields=('author', 'title', ), name='unique_title_review')]
class Comment(BasePost):
review = models.ForeignKey(
Review, on_delete=models.CASCADE, related_name='comments')
urls.py
router_v1.register(
r'^titles/(?P<title_id>\d+)/reviews', ReviewViewSet, basename='review')
router_v1.register(
r'^titles/(?P<title_id>\d+)/reviews/(?P<review_id>\d+)/comments',
CommentViewSet, basename='comment')
views.py
class ReviewViewSet(BasePostViewSet):
serializer_class = ReviewSerializer
def get_queryset(self):
return self.get_title().reviews.all()
def perform_create(self, serializer):
serializer.save(author=self.request.user, title=self.get_title())
def get_title(self, key='title_id'):
return get_object_or_404(Title, id=self.kwargs.get(key))
class CommentViewSet(BasePostViewSet):
serializer_class = CommentSerializer
def get_queryset(self):
return self.get_review().comments.all()
def perform_create(self, serializer):
serializer.save(author=self.request.user, review=self.get_review())
def get_review(self, key='review_id'):
return get_object_or_404(Review, id=self.kwargs.get(key))
serializers.py
class ReviewSerializer(BasePostSerializer):
title = serializers.HiddenField(default=None)
class Meta:
model = Review
fields = ('id', 'text', 'author', 'score', 'pub_date', 'title', )
validators = [UniqueTogetherValidator(
queryset=Review.objects.all(), fields=('author', 'title', ))]
class CommentSerializer(BasePostSerializer):
class Meta:
model = Comment
fields = ('id', 'text', 'author', 'pub_date', )
def create(self, validated_data):
try:
review = Review.objects.create(**validated_data)
except IntegrityError:
raise serializers.ValidationError(
{'detail': 'Вы можете оставить только один отзыв.'})
return review
Currently fixed like that but seems wrong to me.
First, I will assume that BasePostViewSet does inherits from CreateModelMixin. My assumption is based on the fact that you are redefining perform_create:
def perform_create(self, serializer):
serializer.save()
According to DRF Documentation on passing additional argumernts to the .save() method:
Any additional keyword arguments will be included in the validated_data argument when .create() or .update() are called.
The original .create() method from the CreateModelMixin:
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
self.perform_create(serializer)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
It does the validation before calling the .perform_create() method, hence your redefined version of .perform_create:
def perform_create(self, serializer):
serializer.save(author=self.request.user, review=self.get_review())
Gets called after validation, hence, your serializer validator is not checked with your desired arguments.
Now, I think you could achieve this by including your fields in the to_internal_value function data like this (didn't try it out but definitely it runs before validation) (I'm assuming that serializer context is passed accordingly):
def to_internal_value(self, data):
data['author'] = self.context['request'].user
data['review'] = self.context['view'].get_review()
return data
Hopefully this helps.
I am trying to add pagination using super().list() method in modelviewset
class RecentlyViewedVideosViewSet(ResponseViewMixin, mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet):
queryset = RecentlyViewedVideos.objects.all()
serializer_class = RecentlyViewedVideosSerializer
def list(self, request, **kwargs):
print('list')
try:
if 'learner_id' in self.kwargs:
learner_id = self.kwargs['learner_id']
else:
learner_id = self.request.learner.id
response_data = super().get_queryset().filter(learner_id=learner_id)
print(response_data)
serializer = RecentlyViewedVideosSerializer(response_data, many=True)
return self.jp_response(s_code='HTTP_200_OK', data=serializer.data)
in output, it displays all the documents in the table, but I only need those details in the "details", give me a way to get the exact output.
It might be better to do this in the .get_queryset(…) method:
def get_queryset(self):
if 'learner_id' in self.kwargs:
learner_id = self.kwargs['learner_id']
else:
learner_id = self.request.learner.id
return super().get_queryset().filter(learner_id=learner_id)
then the boilerplate code to filter, paginate, etc. the view are still implemented by the .list(…) method of the ListModelMixin.
Whats the best way to do view permissions in DRF based on user type currently?
In my structure there are several user_types and for example TEAM_LEADER cant create a team object but can see the list of teams. Which means for the same class view i want to use different permissions for POST and GET for example.
I'm looking to do this as dry as possible and i'm trying to follow the skinny view fat models design principle(also wondering if that's good practice to follow in 2021).
models.py for the user model
class User(AbstractBaseUser):
...fields here
objects = UserManager()
USERNAME_FIELD = "email"
def __str__(self):
return self.email
def has_perm(self, perm, obj=None):
if perm.Meta.verbose_name=="worksite" and perm.request.method =="POST":
if self.user_type <= self.DEPARTMENT_MANAGER:
return True
else:
return False
return True
views.py
class DashboardPermissions(BasePermission):
message="You dont have permission for this action"
def has_permission(self, request, view):
return request.user.has_perm(view.Meta.verbose_name)
class ViewName(CreateAPIView):
permission_classes = (IsAuthenticated,DashboardPermissions)
authentication_classes = ()
serializer_class = WorksiteSerializer
queryset = Worksite.objects.all()
class Meta:
verbose_name="view_name"
def get(self, request, *args, **kwargs):
return self.list(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
return self.create(request, *args, **kwargs)
Bonus question would my solution create any performance issues?
Creating your custom Permission class is good practice. So that part looks OK to me. We could debate on whether the logic should be in the Permission or the User (like you did), but that's not a big deal.
If you want to have different permissions for different endpoints within your view, simply override the get_permissions method.
# Inherited method from APIView
def get_permissions(self):
"""
Instantiates and returns the list of permissions that this view requires.
"""
return [permission() for permission in self.permission_classes]
As you can see, for ALL services, it will fetch the permissions from self.permission_classes.
To use different permissions between GET/CREATE, you could create a dict of endpoint: [...permissions] and override get_permissions to fetch the one matching the current action
permissions = {
"create": [P1, P2,],
"get": [P1,]
}
def get_permissions(self):
action = self.it_is_somewhere_in_there
return [permission() for permissions in self.permissions[action]]
#JordanKowal's answer is correct, but as mentioned in the comments,
Also then i'd be repeating the permissions dict a lot ? in order to do it for every class of view right
For this you can create a mixin class. What it essentially allows you to do is move some code/feature that is to duplicated in multiple views to a standalone class and just inherit from it as per your convenience.
To extend on Jordan's answer, here's what a mixin class would look like:
class DefaultPermissionsMixin(object):
permissions = {
"create": [IsAuthenticated, DashboardPermissions],
"get": [DashboardPermissions]
}
def get_permissions(self):
# default `get_permissions` method
# reads `self.permission_classes`
perms = super().get_permissions()
if self.action in self.permissions.keys():
return perms + [p() for p in self.permissions[self.action]]
else:
return perms
class View1(CreateAPIView, DefaultPermissionsMixin):
# ...snip...
class View2(CreateAPIView, DefaultPermissionsMixin):
# i can overwrite here per my convenience
permissions = {
"create": [DashboardPermissions],
"delete": [],
}
# i can also define permissions the default way
# that will be enabled on all actions
permission_classes = [IsAuthenticated]
# ...snip...
i need to use a path with an optional paramentr, to specify a user via a srting a request would look like 'api/users/specific_username' or 'api/users' for all users
urls:
router = DefaultRouter()
router.register(r'users', MyUserViewSet, basename='user-me')
views:
class UserViewSet(viewsets.ModelViewSet):
serializer_class = UserSerializer
def get_queryset(self):
queryset = User.objects.all()
if self.kwargs['username']:
username=self.request.GET.get('username')
queryset = User.objects.filter(username=username)
return queryset
username=self.kwargs['username']
returns KeyError
username=self.request.GET.get('username')
returns None
I've managed to achieve that by doing like so:
so for request that looks like so:
http://example.com/api/viewset?username=denvercoder9
the code will look this:
def get_queryset(self):
"""
Optionally restricts the returned purchases to a given user,
by filtering against a `username` query parameter in the URL.
"""
queryset = Purchase.objects.all()
username = self.request.query_params.get('username', None)
if username is not None:
queryset = queryset.filter(purchaser__username=username)
return queryset
or if you really want you override the retrieve:
(haven't tested this code)
def retrieve(self, request, pk=None):
queryset = User.objects.filter(username=pk)
contact = get_object_or_404(queryset, pk=1)
serializer = ContactSerializer(contact)
return Response(serializer.data)
So What I've been trying to do is to have my API view only return objects that have their attributes post_user to the current id of the logged in user. These post_user attributes are populated as whenever I post it populates the variable with the current user's id through my serializer.
However, I am not successful as it says request is not defined. I just want to get the current user's id so that I can use it to filter my object returns
views.py
# To retrieve and list all posts with DRF
class ListPosts(generics.ListCreateAPIView):
queryset = Posts.objects.get(post_user=request.user.id)
serializer_class = PostsSerializer
permission_classes = (permissions.IsAuthenticated,)
serializers.py
# serializer for posts to be taken
class PostsSerializer(serializers.ModelSerializer):
class Meta:
model = Posts
fields = ('id','post_title','post_content',)
def create(self, validated_data):
posts = Posts.objects.create(
post_title=validated_data['post_title'],
post_content=validated_data['post_content'],
# gets the id of the current user
post_user=self.context['request'].user.id,
)
posts.save()
return posts
error is in line
queryset = Posts.objects.get(post_user=request.user.id)
here request is not define at class declaration time. Solution is you can override the get_queryset method.
class ListPosts(generics.ListCreateAPIView):
queryset = Posts.objects.all()
serializer_class = PostsSerializer
permission_classes = (permissions.IsAuthenticated,)
def get_queryset(self, *args, **kwargs):
return Posts.objects.filter(post_user=self.request.user)
Inherit CreateModelMixin's features inside PostsSerializer and try to define your create() method like def create(request, *args, **kwargs).
Finally, you can try to get user id using request.user.id.
For a better documentation, you can check https://www.django-rest-framework.org/api-guide/generic-views/.
Also check what are Mixins and why do we use it (if you do not know).
For a little and brief definition, Mixins are just class with methods that can be mostly inherited and used by our views.
If you have any doubt, please comment.