Django - Add object in front of existing QuerySet - python

I am building a library Django application where I have a BooksView to display all Books written by a certain Author. Under certain conditions, I want a single Book to be displayed before all other books in the query set. This should happen when the id of the book appears in the URL, otherwise, all books should be listed in chronological order.
books/urls.py:
urlpatterns = [
# Books displayed in chronological order
path("<slug:author>/books", views.BooksView.as_view()),
# Books displayed in chronological order
# but first book is the one with id `book`
path("<slug:author>/books/<int:book>", views.BooksView.as_view()),
]
books/views.py:
class BooksView(APIView):
def get(self, request, author, book=None):
author = get_object_or_404(Author, slug=author)
books = author.author_books
books = books.order_by("-pub_date")
if book:
book = get_object_or_404(Book, id=book)
# TODO: add `book` in front of books QuerySet (and remove duplicated if any)
serializer = BooksSerializer(books, many=True)
return Response(serializer.data)
How can I accomplish my TODO? How can I push an object in front of an existing query set?

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)

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])

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)

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.

Related

Wagtail Page model transitive search on custom fields [duplicate]

I'm using Wagtail, and I want to filter a selection of child pages by a Foreign Key. I've tried the following and I get the error django.core.exceptions.FieldError: Cannot resolve keyword 'use_case' into field when I try children = self.get_children().specific().filter(use_case__slug=slug):
class AiLabResourceMixin(models.Model):
parent_page_types = ['AiLabResourceIndexPage']
use_case = models.ForeignKey(AiLabUseCase, on_delete=models.PROTECT)
content_panels = ArticlePage.content_panels + [
FieldPanel('use_case', widget=forms.Select())
]
class Meta:
abstract = True
class AiLabCaseStudy(AiLabResourceMixin, ArticlePage):
pass
class AiLabBlogPost(AiLabResourceMixin, ArticlePage):
pass
class AiLabExternalLink(AiLabResourceMixin, ArticlePage):
pass
class AiLabResourceIndexPage(RoutablePageMixin, BasePage):
parent_page_types = ['AiLabHomePage']
subpage_types = ['AiLabCaseStudy', 'AiLabBlogPost', 'AiLabExternalLink']
max_count = 1
#route(r'^$')
def all_resources(self, request):
children = self.get_children().specific()
return render(request, 'ai_lab/ai_lab_resource_index_page.html', {
'page': self,
'children': children,
})
#route(r'^([a-z0-9]+(?:-[a-z0-9]+)*)/$')
def filter_by_use_case(self, request, slug):
children = self.get_children().specific().filter(use_case__slug=slug)
return render(request, 'ai_lab/ai_lab_resource_index_page.html', {
'page': self,
'children': children,
})
I've seen this answer, but this assumes I only have one type of page I want to filter. Using something like AiLabCaseStudy.objects.filter(use_case__slug=slug) works, but this only returns AiLabCaseStudys, not AiLabBlogPosts or AiLabExternalLinks.
Any ideas?
At the database level, there is no efficient way to run the filter against all page types at once. Since AiLabResourceMixin is defined as abstract = True, this class has no representation of its own within the database - instead, the use_case field is defined separately for each of AiLabCaseStudy, AiLabBlogPost and AiLabExternalLink. As a result, there's no way for Django or Wagtail to turn .filter(use_case__slug=slug) into a SQL query, since use_case refers to three different places in the database.
A couple of possible ways around this:
If your data model allows, restructure it to use multi-table inheritance - this looks fairly similar to your current definition, except without the abstract = True:
class AiLabResourcePage(ArticlePage):
use_case = models.ForeignKey(AiLabUseCase, on_delete=models.PROTECT)
class AiLabCaseStudy(AiLabResourcePage):
pass
class AiLabBlogPost(AiLabResourcePage):
pass
class AiLabExternalLink(AiLabResourcePage):
pass
AiLabResourcePage will then exist in its own right in the database, and you can query its use_case field with an expression like: AiLabResourcePage.objects.child_of(self).filter(use_case__slug=slug).specific(). There'll be a small performance impact here, since Django has to pull data from one additional table to construct these page objects.
Run a preliminary query on each specific page type to retrieve the matching page IDs, before running the final query with specific():
case_study_ids = list(AiLabCaseStudy.objects.child_of(self).filter(use_case__slug=slug).values_list('id', flat=True))
blog_post_ids = list(AiLabBlogPost.objects.child_of(self).filter(use_case__slug=slug).values_list('id', flat=True))
external_link_ids = list(AiLabExternalLink.objects.child_of(self).filter(use_case__slug=slug).values_list('id', flat=True))
children = Page.objects.filter(id__in=(case_study_ids + blog_post_ids + external_link_ids)).specific()
Try:
children = self.get_children().filter(use_case__slug=slug).specific()

How to show only one object in django admin list view?

Problem
I wish to show only the last row of a QuerySet based on the ModelAdmin's ordering criteria. I have tried a couple of methods, but none has worked for me.
Model:
class DefaultConfig(models.Model):
created_at = models.DateTimeField()
...
Attempt 1:
I tried overriding the ModelAdmin's get_queryset method and slicing super's result, but I came up with some issues.
I.E:
class DefaultConfigAdmin(models.ModelAdmin):
model = Config
ordering = ('-created_at',)
def get_queryset(self, request):
qs = super().get_queryset(request)
return qs[<slice>]
I tried the following values for [<slice>]s:
[-1:]: raised an Exception because negative slicing is not supported
[:1]: raised AssertionError: Cannot reorder a query once a slice has been taken.
Attempt 2:
I tried obtaining the max value for created_at and then filtering for records with that value. I.E:
class DefaultConfigAdmin(models.ModelAdmin):
model = Config
def get_queryset(self, request):
qs = super().get_queryset(request)
return qs.annotate(max_created_at=Max('created_at')).filter(created_at=F('max_created_at'))
But silly me, that works at row level, so it will only return aggregates over the row itself.
Further attempts (TBD):
Perhaps the answer lies in SubQuerys or Windowing and ranking.
Is there a more straight forward way to achieve this though?
Did you try to set list_per_page = 1?
class DefaultConfigAdmin(models.ModelAdmin):
model = Config
ordering = ('-created_at',)
list_per_page = 1
Technically this will still return all Config objects, but only one per page and the latest one will be on the first page.
Another solution (similar to your "Attempt 2"), which involves an extra query, is to manually get hold of the latest created_at timestamp and then use it for filtering.
class DefaultConfigAdmin(models.ModelAdmin):
model = Config
def get_queryset(self, request):
qs = super().get_queryset(request)
latest_config = qs.order_by('created_at').last()
return qs.filter(created_at=latest_config.created_at)

changeing ListView get_queryset? pagination in django

Good day everyone.
I am having some trouble with the paginator in django.
I have in my db all the customers info, so what I want to do is to display that info, but I made a search function in which you clic a letter button, for example you clic A and it will filter all the customers that their last name starts with the letter A, if you clic B , it will filter all customers with their last name start with B, and so on. This works correctly, the problem is that I also want to display 10 customers per page, so if I have 20 customers that their last name starts with the letter A, what you will see, will be 10 customers and a bar that says ( <<< page 1 page 2 >>> ) or something like that, and that should be solved with the paginator, I added it, but its not working.
I think the problem is that maybe my get function is rewriting the get_query function from ListView perhaps? I tryed different things but I'm not sure.
Here is my code in views:
class ExpedientView(ListView):
queryset = Portfolio.objects.filter(products__isnull=True).order_by('owner__last_name')
template_name = 'dashboard-admin/portfoliorecords.html'
paginate_by = 10
def get(self,request):
if request.GET['letter'] == '':
context_object_name = 'portfolios'
queryset = Portfolio.objects.filter(products__isnull=True).order_by('owner__last_name')
context = queryset
else:
letter = request.GET['letter']
context_object_name = 'portfolios'
queryset = Portfolio.objects.filter(products__isnull=True).order_by('owner__last_name').filter(owner__last_name__istartswith=letter)
context = queryset
return render(request, 'dashboard-admin/portfoliorecords.html', {'portfolios': context})
the get(self,request) function, works perfectly, however the first part where the paginated_by is not working.
Before I added the get function, and just filtering all the customers, the paginator worked fine, so in the template, the code works properly.
You have to modify the def get_queryset(self) method not the def get(self, request) method
Remove the def get(self, request) method and the below code.
def get_queryset(self):
queryset = Portfolio.objects.filter(products__isnull=True).order_by('owner__last_name')
letter = request.GET.get('letter', None)
if letter:
queryset.filter(owner__last_name__istartswith=letter)
return queryset
If all you need to do is dynamically change the queryset then instead of overriding get (Which calls the appropriate functions in the view, which will perform all pagination tasks, etc.) you should be overriding get_queryset:
class ExpedientView(ListView):
queryset = Portfolio.objects.filter(products__isnull=True)
template_name = 'dashboard-admin/portfoliorecords.html'
paginate_by = 10
ordering = 'owner__last_name' # Put the ordering here instead of specifying it in the queryset
def get_queryset(self):
queryset = super().get_queryset()
letter = self.request.GET.get('letter')
if letter:
queryset = queryset.filter(owner__last_name__istartswith=letter)
return queryset

Django concatenate list queries from two models

As a Django newbie, I am trying to return JSON Objects from two models with each object containing the username, id, ticketID. Right now the code is simply putting the lists together without indexing. I should point out that there is a relationship between user and ticket so that can be traversed also.
{"username":"Paul","id":2}, {"username":"Paul","id":2}, {"username":"Ron","id":19}, {"id":"1c6f039c"}, {"id":"6480e439"},
{"id":"a97cf1s"}
class UsersforEvent(APIView):
def post(self, request):
body_unicode = request.body.decode('utf-8')
body = json.loads(body_unicode)
value = body['event']
queryset = Ticket.objects.filter(event = value)
referenced_users = User.objects.filter(ticket_owner__in=queryset.values('id'))
result_list = list(itertools.chain(referenced_users.values('username', 'id'), queryset.values('id')))
return Response((result_list))
You should do a single query to get the ticket with the related user for each one, then create the list of dicts from there. Assuming your Ticket model has an "owner" field which is a FK to User:
queryset = Ticket.objects.filter(event=value).select_related('owner')
result_list = [{'ticket_id': ticket.id, 'username': ticket.owner.username, 'user_id': ticket.owner.id}
for ticket in queryset]
(Note, this shouldn't be the action of a POST though; that is for changing data in the db, not querying.)

Can I use the existing oscar oscar-api object model to implement a single-item only basket?

I'm working on a modified checkout view that accepts a SINGLE product/price/quantity, customer info (name, address, email) and shipping charges.
QUESTION: Can I use the existing django-oscar-api methods for this? In other words, I'd like to enforce a basket model that contains only a single product and uses (LIFO) last in first out when making updates.
Here is my first attempt at it:
# This method is a hybrid of two oscarapi methods
# 1. oscarapi/basket/add-product
# 2. oscarapi/checkout
# It only allows one product to be added to the
# basket and immediately freezes it so that the
# customer can checkout
def post(self, request, format=None):
# deserialize the product from json
p_ser = self.add_product_serializer_class(
data=request.data['addproduct'], context={'request': request})
if p_ser.is_valid():
# create a basket
basket = operations.get_basket(request)
# load the validate the product
product = p_ser.validated_data['url']
# load the validated quantity
quantity = p_ser.validated_data['quantity']
# load any options
options = p_ser.validated_data.get('options', [])
#validate the basket
basket_valid, message = self.validate(
basket, product, quantity, options)
if not basket_valid:
return Response(
{'reason': message},
status=status.HTTP_406_NOT_ACCEPTABLE)
# add the product to the validated basket
basket.add_product(product, quantity=quantity, options=options)
# apply offers
operations.apply_offers(request, basket)
###### from oscarapi.views.checkout
# deserialize the checkout object and complete the order
co_data = request.data['checkout']
co_data['basket'] = "http://127.0.0.1:8000/oscar-api/baskets/"+str(basket.pk)+"/"
c_ser = self.checkout_serializer(
data=co_data, context={'request': request})
if c_ser.is_valid():
order = c_ser.save()
basket.freeze()
o_ser = self.order_serializer_class(
order, context={'request': request})
oscarapi_post_checkout.send(
sender=self, order=order, user=request.user,
request=request, response=response)
return response.Response(o_ser.data)
return response.Response(c_ser.errors, status.HTTP_406_NOT_ACCEPTABLE)
The raw answer is yes.
Now ...
As you surely have noticed, implementing this using the oscar-api is not a trivial task. oscar-api it just not implemented thinking in such features. It is a REST API, nothing else.
The simplest way ...
I would recommend to go deeper and fork the basket app and overwrite the Basket.add_product method to ensure what you want, simple.
0f course this is only valid if you own the backend implementation or have some decision power over it.
Look this:
class Basket(AbstractBasket):
# ...
def add_product(self, product, quantity=1, options=None):
if quantity > 1:
# Throw an error, set it to 1, is up to you
# Remove all lines.
self.flush()
# Do as usual.
super(Basket, self).add_product(product, quantity, options)
very very simple!

Categories

Resources