[Django]-Django – Add object in front of existing QuerySet

2👍

This doesn’t precisely answer your question but I think it would allow you to get the desired outcome: don’t worry about preserving the queryset and instead transform it into a list.

This isn’t a thing you want to do for every queryset in your entire codebase especially if it’s a queryset that you might later want to build upon and compose with other filters, excludes, etc. But when you’re about to render a page (like you are here) it is OK because you’re about to cause the queryset to be evaluated anyhow. You’re causing it to happen perhaps microseconds sooner.

class BooksView(APIView):

    def get(self, request, author, book=None):

        author = get_object_or_404(Author, slug=author)
        books = list(author.author_books)

        if book:
            book = Book.objects.get(id=book)
            # TODO: add `book` in front of books QuerySet (and remove duplicated if any)
            books = [x for x in books if not x == book] # first remove if dup
            books.insert(0, book) # now insert at front

        serializer = BooksSerializer(books, many=True)
        return Response(serializer.data)

EDIT 1

The BooksSerializer (which subclasses the BaseSerializer I suspect) is going to make a list anyhow as soon as you call it:

    def to_representation(self, data):
        """
        List of object instances -> List of dicts of primitive datatypes.
        """
        # Dealing with nested relationships, data can be a Manager,
        # so, first get a queryset from the Manager if needed
        iterable = data.all() if isinstance(data, models.Manager) else data

        return [
            self.child.to_representation(item) for item in iterable
        ]

https://github.com/encode/django-rest-framework/blob/master/rest_framework/serializers.py#L663

EDIT 2

What about trying this instead? By adding the exclude before the queryset is evaluated into a list you prevent the O(n) scan through the list to find and remove the "main" book that’s supposed to be at the top.

class BooksView(APIView):

    def get(self, request, author, book=None):

        author = get_object_or_404(Author, slug=author)
        books = author.author_books

        if book:
            book = Book.objects.get(id=book)
            # TODO: add `book` in front of books QuerySet (and remove duplicated if any)
            # ensure the queryset doesn't include the "main" book
            books = books.exclude(book_id=book.id)
            # evaluate it into a list just like the BookSerializer will anyhow
            books = list(books)
            # now insert the "main" book at the front of the list 
            books.insert(0, book)

        serializer = BooksSerializer(books, many=True)
        return Response(serializer.data)

1👍

You can use the bitwise | operator as

from django.http.response import Http404


class BooksView(APIView):

    def get(self, request, author, book=None):
        author = get_object_or_404(Author, slug=author)
        book_qs = author.author_books
        if book:
            single_book_qs = Book.objects.filter(id=book)
            if not single_book_qs.exists():
                raise Http404
            book_qs = single_book_qs | book_qs

        serializer = BooksSerializer(book_qs, many=True)
        return Response(serializer.data)

Please note that one caveat of this solution is that, if you use the order_by(...) method, the position will be changed according to the ORDER By expression.

Update 1

Since you are using an order_by(...) expression, you must do something like this,

class BooksView(APIView):

    def get(self, request, author, book=None):
        author = get_object_or_404(Author, slug=author)
        book_qs = author.author_books.order_by("-pub_date")
        serialized_single_book = []
        if book:
            single_book = get_object_or_404(Book, id=book)
            book_qs.exclude(id=book) # to remove dups
            serialized_single_book = [BooksSerializer(single_book).data]

        serializer = BooksSerializer(book_qs, many=True)
        serialized_book_qs = serializer.data
        return Response([*serialized_single_book, *serialized_book_qs])
👤JPG

1👍

This should do it, just use the union method in the QuerySet object, and exclude
the book we are trying to access from books queryset.

I did some minor changes to the code, but you don’t need to do more than that, to accomplish what you need.

class BooksView(APIView):

    def get(self, request, author, book=None):
        author = get_object_or_404(Author, slug=author)
        books = author.author_books.all().order_by('-pub_date')

        if book:
            # I am assuming this line if for validating that the request is valid.
            # I've changed the query to also include author's slug,
            # so the request doesn't get books not related to the author.
            book_obj = get_object_or_404(Book, id=book, author__slug=author)

            # Just Query the same book, and union it with the books queryset,
            # excluding the current book in the book_obj
            books = Book.objects.filter(id=book_obj.id).union(books.exclude(id=book_obj.id))

        serializer = BooksSerializer(books, many=True)
        return Response(serializer.data)

0👍

A little out of the box maybe, but if you want to keep it as queryset, then this is the solution. Please note that this solves the X problem, not the Y problem. The goal is to put one book first in the list, which can be achieved by insertion, but also by reordering based on an annotation.

from django.db.models import Case, When, Value, IntegerField
from rest_framework import generics

from . import models, serializers


class BooksPerAuthor(generics.ListAPIView):
    serializer_class = serializers.BookWithoutAuthor

    def get_queryset(self):
        book_pk = self.kwargs.get("book", 0)
        queryset = (
            models.Book.objects.filter(author__slug=self.kwargs["author"])
            .annotate(
                promoted=Case(
                    When(pk=book_pk, then=Value(1)),
                    default=Value(0),
                    output_field=IntegerField(),
                )
            )
            .order_by("-promoted", "-pub_date")
        )
        return queryset

This doesn’t catch non-existing references – not a fan of returning 404 for list views and in case the promoted book is not in the collection, then there’s no real harm done. Plenty of examples above to do the same thing with 404’s at the (minor) cost of additional queries.

Leave a comment