Django Rest Framework: Respond with 404 if no result found - django

With the help of some others here I managed to get my DRF driven API to a good point to work with. I'm now trying to mingle out some details. Currently.
Here's the view:
class EpisodeViewSet(viewsets.ModelViewSet):
queryset = Episode.objects.all().order_by('-published_at')
filter_backends = (DjangoFilterBackend,)
filter_fields = ('show_id', 'number')
def get_serializer_class(self):
if self.action == 'retrieve':
return EpisodeDetailSerializer
if self.request.GET.get('show_id') and self.request.GET.get('number'):
return EpisodeDetailSerializer
return EpisodeSerializer
and here's my serializer:
class EpisodeDetailSerializer(serializers.ModelSerializer):
chapters = ChapterMarkSerializer(source='chaptermark_set', many=True)
media = MediaClipSerializer(source='mediaclip_set', many=True)
show = ShowSerializer()
guest = GuestSerializer(source='guest_set', many=True, read_only=True)
topic = TopicSerializer(source='topic_set', many=True, read_only=True)
# def get_queryset(self):
# show = self.request.query_params.get('show')
# number = self.request.query_params.get('number')
# queryset = self.objects.get(show_id=show, number=number)
# return queryset
class Meta:
model = Episode
fields = ('id', 'title', 'number', 'subtitle', 'show', 'published_at', 'updated_at', 'description_title',
'description', 'show_notes', 'supporters_title', 'supporters_text', 'supporters_button',
'forum_title', 'forum_text', 'forum_link', 'cover_image', 'updated_at', 'chapters', 'media',
'guest', 'topic')
depth = 1
I'm not sure anymore where the get_queryset function came from but the results weirdly are the same with and without, that's why it's commented out at the moment.
Background is I'm querying episodes in two different ways:
through their primary key api/episodes/123
through a filter by show and number api/episodes?show_id=1&number=2
In the first case the api returns one object if found or a 404 with an object only containing
{
"detail": "Not found."
}
in case an episode with that ID doesn't exist.
In the second case it returns a list of objects (sidenote: I'd prefer to also only get one object as there will never be more than one result but that's probably a different topic). and it returns an empty result still with HTTP 200 if nothing is found.
For cleaner handling on the frontend side, I'd prefer to raise an HTTP 404 if nothing was found in both cases. I already found that this may work with a try/except in Django but I didn't yet succeed in finding where to place it. I'M still only scratching the surface with DRF.
Any help is appreciated.

You can override viewset's list method for this:
from rest_framework import status
from rest_framework.response import Response
def list(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset())
if not queryset.exists():
return Response({"detail": "Not found."}, status=status.HTTP_404_NOT_FOUND)
page = self.paginate_queryset(queryset)
if page is not None:
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)

I think the easiest way for you (and maybe the most general) will be to override the list() method of ModelViewSet. This is the code (from mixins.py):
def list(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset())
page = self.paginate_queryset(queryset)
if page is not None:
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
Above is the original code. You can then override it in your class, for example, you can add the following lines (after queryset = and before page =)
...
if queryset.count():
raise exceptions.NotFound()
...
Actually, you can do what ever you want now. You can also change the function to return single object.

Related

Global Pagination not working on DRF Project

I have written an API on DRF which returns a list of data based on certain conditions, but the data is very large and global pagination is not applying on it. As a result, speed slows down and therefore, data is not shown properly on a single page.
I have adding following code in settings.py file:
REST_FRAMEWORK = {
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
"PAGE_SIZE": 10
}
This is my API:
class TeacherViewSet(ModelViewSet):
queryset = Teacher.objects.all()
serializer_class = serializers.TeacherSerializer
authentication_classes = [TokenAuthentication]
def list(self, request, *args, **kwargs):
response = []
for teacher in queryset:
name = Student.objects.filter(teacher=teacher).values("name")
res = {"name": name}
response.append(res)
return Response(response)
Any thing wrong I am doing?
Since you are overriding list method you are disabling pagination feature. Default list method looks like this:
def list(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset())
page = self.paginate_queryset(queryset)
if page is not None:
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
Note paginate_queryset and get_paginated_response methods which perform pagination. So if you need to override list you should include these methods as well:
def list(self, request, *args, **kwargs):
response = []
queryset = self.filter_queryset(self.get_queryset())
queryset = self.paginate_queryset(queryset)
for teacher in queryset:
name = Student.objects.filter(teacher=teacher).values("name")
res = {"name": name}
response.append(res)
return self.get_paginated_response(response)
Not related to original question but please note that performing DB query inside a loop is considered a bad practice and could affect performance of your view.
Instead of fetching student for each teacher inside for loop consider to use prefetch_related.

Supporting search queries in Django Rest Framework

Im currently trying to allow users to search my database from the client side and return the response to them. I'm a little confused on where in the view I need to support the search query, or if the way I'm going about it is even correct. I've done some research already and found out about the SearchFilter from DRF but unsure how to incorporate it into my code.
Here is my views for the resource i'm trying to search:
class Recipes(ViewSet):
def list(self, request):
recipes = Recipe.objects.all()
user = self.request.query_params.get('user', None)
if user is not None:
recipes = recipes.filter(author = user)
serializer = RecipeSerializer(recipes, many=True, context={'request': request})
return Response(serializer.data)
def retrieve(self, request, pk=None):
try:
recipe = Recipe.objects.get(pk=pk)
serializer = RecipeSerializer(recipe, context={'request': request})
return Response(serializer.data)
except Exception as ex:
return HttpResponseServerError(ex)
Based on my research I've found that I can use:
serializer_class = RecipeSerializer
queryset = Recipe.objects.all()
filter_backends = (SearchFilter,)
filter_fields = ('title', 'ingredients')
in some way but i'm not exactly sure how. Any guidance is appreciated

Django passing multiple ids through URL

For POST/GET (etc) requests I have the following URL for one user:
v1/users/userid123
registered as so:
router.register(r"v1/users", accounts_views_v1.UserViewSet)
What modifications should I make so that I can pass multiple user IDs like:
v1/users/?ids=["userid123","userid456"]?
It worked without any modification for another model of mine, with the same criteria, but this one keeps giving a 403 error (before even going into the method!) when I try it
Code per request:
My viewset is insanely long, here's the beginning though
class UserViewSet(MultipleDBModelViewSet):
serializer_class = UserSerializer
queryset = User.objects.none()
#workspace_specific
def get_queryset(self):
group_ids = json.loads(self.request.query_params.get("group_id", "[]"))
queryset = None
if group_ids:
queryset = User.objects.filter(group_memberships__group__in=group_ids).distinct()
else:
queryset = User.objects.all()
return queryset.select_related("workspace_role").prefetch_related(
"group_memberships__group_role", "group_memberships__group"
)
and my URLS:
PREFIX = settings.REST_FRAMEWORK_ROUTER_PREFIX
if PREFIX:
PREFIX = r"^" + str(PREFIX) + r"/"
router = BulkRouter()
single_object_router = SingleObjectRouter()
lazy_single_object_create_or_update_router = LazySingleObjectCreateOrUpdateRouter()
...
router.register(r"v1/users", accounts_views_v1.UserViewSet)
...
urlpatterns = [...]
GET method:
def list(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset())
page = self.paginate_queryset(queryset)
if page is not None:
serializer = self.get_serializer(page, many=True, exclude=None)
return self.get_paginated_response(serializer.data)
serializer = self.get_serializer(queryset, many=True, exclude=None)
return Response(serializer.data)
I found the kink, this function wraps all the others:
def permission_check_wrapper(self, request, *args, pk=None, **kwargs)
of course, when there are IDs instead of one pk, it doesn't work - how can I pass ids instead?

How to apply a djngoFilterBackend in a detail_route() viewset

I want to use django filters in a view which has some other behaviour, without returning viewset retrieve or list methods.
My code is the following:
class ArticleView(ReadOnlyModelViewSet):
serializer_class = get_serializer_class(Article)
queryset = Article.objects.all()
filter_backends = (filters.DjangoFilterBackend,)
filter_fields = ('TYPE',)
#detail_route()
def articles(self, request, pk=None):
some_behaviour()
return MY QUERYSET (which can or cannot be modified) FILTERED
so by hitting /api/articles and its derivates the queryset gets filtered correctly, also if i return self.retrieve(request) (obviously), but i am not able to modify my queryset. Then my question is, what is needed to apply django filters EXPLICITLY in that situation, or how can i tell him to do that instead of doing request.query_string.pop(bla bla bla).
Thanks!
I've recently got similar problem.
I've found ViewSet.filter_queryset() function that is doing exactly that.
Remember that ViewSet.get_object() also use self.filer_queryset() so make Your detailed_route method usinq get_object_by_pk() like:
class ViewSet:
def get_object_by_pk(self, pk):
return self.get_queryset().get(pk=pk)
#detail_route():
def something(self, request, pk):
object = self.get_object_by_pk(pk)
queryset = object.whatevet_set
page = self.paginate_queryset(self.filter_queryset(queryset))
if page is not None:
serializer = SerializerClass(page, many=True, context={'request': request})
return self.get_paginated_response(serializer.data)
serializer = SerializerClass(queryset, many=True, context={'request': request})
return Response(serializer.data)

Order data by non-database field

I'm using Django rest framework, Here is my serializers.py for social app:
class SocialPostSerializer(serializers.ModelSerializer):
likes = serializers.SerializerMethodField() # define field
class Meta:
model = SocialPost
def get_likes(self, obj):
post_id = obj.id
#I get post_like from django-redis
post_like = get_redis_connection("default")
likes = post_like.get("post"+":"+str(post_id))
if likes == None:
return 0
else:
likes = likes.decode('utf-8')
return likes
With the code above, I got what I need from the API.
Since 'likes' doesn't exist in my database(Mysql here), I can't using order_by('likes') to sort the data with django ORM
I follow the doc here ListCreateAPIView which lead me to override list(): (I had override create() and get_queryset() before)
from operator import itemgetter
class PostList(generics.ListCreateAPIView):
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
queryset = Post.objects.all()
serializer_class = PostAllSerializer
def list(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset())
page = self.paginate_queryset(queryset)
if page is not None:
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
serializer = self.get_serializer(queryset, many=True)
#problem here
serializer.data = sorted(serializer.data, key=itemgetter(serializer.data['likes']))
return Response(serializer.data)
def create(self, request, *args, **kwargs):
act_id = request.data.get('act')
act = Act.objects.get(pk=act_id)
if act.act_type == 0:
if request.user != act.user:
return Response(status=403)
return super().create(request, args, kwargs)
def perform_create(self, serializer):
serializer.save(user=self.request.user)
def get_queryset(self):
queryset = Post.objects.all().order_by('-post_create_time')
act_id = self.request.query_params.get('act_id', None)
post_author = self.request.query_params.get('post_author', None)
if act_id is not None:
queryset = queryset.filter(act=act_id)
if post_author is not None:
queryset = queryset.filter(user__user_name=post_author)
return queryset
Nothing happened, What is wired is even when I uncomment
return Response(serializer.data)
Still nothing happened, Which part is wrong?
Another question is when I wanna add some extra data like 'question' when I use django FBV:
def results(request, question_id):
question = get_object_or_404(Question, pk=question_id)
return render(request, 'polls/results.html', {'question': 'some data I wanna add'})
Is it possible to add the data in serializer.data? For example, I wanna display user_id from which user request this api:
def list(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset())
page = self.paginate_queryset(queryset)
if page is not None:
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
user_id = request.user.id #how can I add the user_id into response data?
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
I look around the docs here Including extra context.
serializer = AccountSerializer(account, context={'request': request})
I don't really understand how to add data in it.
"Since 'likes' doesn't exist in my database(Mysql here), I can't using order_by('likes') to sort the data with django ORM"
You wont be able to sort the results using django ORM as the likes field is not a part of your DB table. The only way to do it is to sort the serializer.data that you get in your view. In your case serializer.data will be a list of dictionary, you can use sort command for list and sort on likes using lambda.
One thing to take care here is as you will be doing the sort by loading the data in memory, make sure that you dont load a lot of data. Have a check on memory utilization.
"Another question is when I wanna add some extra data like 'question' when I use django FBV"
I did'nt understand what is needed here.