Django: Add model object to aggregated queryset - django

Is there a way to add object to aggregated queryset?
For example:
qs = Model.objects.filter(title="abc").aggregate(likes=Count('likes'))
and i want to do something like:
qs = Model.objects.filter(title="abc").aggregate(likes=Count('likes')).get(pk=1)

Whenever a queryset is evaluated, its results are cached. However, this cache does not carry over if you use another method, such as filter or order_by or get, on the queryset. SO you can't try to evaluate the bigger set, and use filtering on the queryset to retrieve the smaller sets without doing another query.
So you can go by doing it this way:
qs = Model.objects.filter(pk=1, title="abc")
if the qs is None you do not have data that matches the query to start with, if it is not None you can go and do it like this:
qs_agg = qs.aggregate(likes=Count('likes'))
qs here is the one that we got from the filtering

Related

Django queryset verify if fields exists

I want to implement a custom ordering backend (using rest_framework.filters.OrderingFilter) and I basically want to check if the field exists for my queryset first before filtering it without evaluating the queryset.
something like
if queryset.is_available_field(my_field):
queryset = queryset.order_by(my_field)
I cannot try / except because I don't know exactly when my queryset will be evaluated.
I found one solution that works for my case but might not cover some edge cases. Also it is not very elegant.
def has_field(queryset, field):
model_fields = [f.name for f in queryset.model._meta.fields]
annotations = list(queryset.query.annotations.keys())
return field in model_fields + annotations

Django filtered Viewset, need to annotate sum over all filtered rows. Group by "all"?

There are hundreds of questions here on various django annotate/aggregate constructions and filters but I couldn't find this simple use-case asked (or answered).
I have a "Payments" model and associated ListAPIView ViewSet endpoint with nicely setup DjangoFilters so the client can filter on created__lte, created__gte, company= etc.
Now I want to add an endpoint that derives from the above but only returns the sum of some fields and count of the total filtered objects.
I know exactly how to do this if I would just write the View "from scratch" (I can just hack the DRF View into executing get_queryset().aggregate() then feeding into a serializer and returning), but I want to do it in a "Django-way" if possible.
For example, combined with a serializer that defines "total_amount" and "nbr", this (almost) works:
queryset = models.Payment.objects.values('company').annotate(total_amount=Sum('amount'),
nbr=Count('id'))
The values() call groups by "company" (a sub-field of Payment), which in combination with annotate() performs a sum by all company payments and annotates with total_amount/nbr. Adding filtering query parameters magically adjusts what goes into the annotation properly.
The problem is, what if I don't want to group (or even filter) by "company", I just want to "group by all"? Is there a way to do that?
I realize this is already a bit magical but the Django-esque way of doing grouping for annotation is this as far as I know.
I also realize I'm probably really better off just by hijacking .retrieve() to evaluate the queryset with .aggregate() tacked at the end and on-the-fly creating the response... still curious though :)
I ended up with the "hack", overriding list() in the View with an .aggregate call and the packing it for the Response through a serializer. This was the most canonical way I could figure out (I mean, re-using as many of the moving parts of Django/DRF as possible like automatic filtering of the queryset, serializing etc).
Bonus: note the Coalesce() wrap which is needed because Sum() doesn't return 0 if the set is empty.
def list(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset())
stats = queryset.aggregate(total_amount=Coalesce(Sum('amount'), 0),
total_vat=Coalesce(Sum('vat'), 0),
nbr_payments=Count('id'))
# .aggregate() returns a dict of the results, not a QuerySet. Wrap it
# into a response through the serializer.
sclass = self.get_serializer_class()
return Response(sclass(stats).data)

prefetch_related and Prefetch object issue, filtering thru reverse Foreign Key

I have 2 models, company and Product.
class Product(Meta):
company = models.ForeignKey(Company, related_name='products', on_delete=models.CASCADE)
form the database I'm trying to get the Company data and the corresponding products.
From the products I want to get only the name and to be ordered descending by updated_at, created_at.
I'm working with Prefetch object and prefetch_related and definitively I have multiple misunderstandings how they work.
def get_queryset(self):
qs = Company.objects.prefetch_related(
Prefetch('products', queryset=Product.objects.only('name').order_by('-updated_at', '-created_at'))).get()
return qs
The error that I receive is:
get() returned more than one Company
Because I closed the prefetch_related method/function with ))) :
I thought get() will act over the Company object and get it using the pk/slug from the url(as get do by default in DetailView). Seems that is not the case.
I'm already using 'products' the related name in the Prefetch object, why in queryset is necessary to tell again the model queryset=Product.objects.... ?
I was looking at the following example in django documentation:
Question.objects.prefetch_related(Prefetch('choice_set')).get().choice_set.all()
If there is 'choice_set' in Prefetch object why is called at the end choice_set.all() ?
Isn't Django attached to the quesryset in prefetch_related the products to the queryset (question.choice_set) ?
I think my problem is that I don't understand the order of execution, and I'm confused how methods are chained, even if are closed by ')'
queryset.get() will only work if the queryset has a single object. If it contains zero or more than one objects, you'll get an error.
You should return a queryset from the get_queryset object. In the class based view, the code that filters on the pk/slug is in get_object.
The prefetch_related method is useful if you want to fetch the products for multiple countries. The way that the Django docs use get() is confusing in my opinion - if the queryset has a single item then prefetch_related is over-complicated.
If you have a single company, then there is no advantage, and the code will be simpler if you fetch the countries separately, e.g. in get_context_data.
def get_context_data(self, **kwargs):
context = super(MyView, self).get_context_data(**kwargs)
context['products'] = Product.objects.filter(company=self.object).Product.objects.only('name').order_by('-updated_at', '-created_at')))
return context
I've removed the only('name') call. It's an optimisation that you probably don't need.
If you really want to use prefetch_related, then remove the get().
qs = Company.objects.prefetch_related(
Prefetch('products', queryset=Product.objects.order_by('-updated_at', '-created_at')))
By specifying the queryset above, you are able to change the order (you could filter it if you like). If you don't want to customize the queryset you can simply do:
Company.objects.prefetch_related('products')
When you use Question.objects.prefetch_related(...), the queryset is still a list of questions. You need to call choice_set.all() on the individual instances to access their choices. This won't cause any additional queries, because Django has already prefetched the choices.
queryset = Question.objects.prefetch_related(Prefetch('choice_set'))
for question in queryset:
print(question) # the question
print(question.choice_set.all()) # the related choices

Get a queryset's current order_by ordering

I'd like to find adjacent items in a queryset given a primary key, as asked here. However I want this to work for any given queryset, that i haven't set the ordering for myself. How can I find the current ordering of a queryset for __gt and __lt filtering?
For example:
queryset = MyModel.objects.all().order_by('some_field')
next_obj = get_next(queryset, after_pk, 10)
...
def get_next(queryset, pk, n):
#ordering is unknown here
obj = queryset.get(pk=pk)
field = queryset.get_order_by_field_name() #???
return queryset.filter(**{field+'__gt': getattr(obj, field)})[:n]
The model may define a default ordering and I can check if the queryset is ordered, but I don't think either are helpful in this case.
You can retrieve the list of columns from order_by clause by accessing queryset.query.order_by property.
From django docs:
The query attribute is an opaque object. It represents the internals of the query construction and is not part of the public API. However, it is safe (and fully supported) to pickle and unpickle the attribute’s contents as described here.

How to filter a query set with the results of another query set in Django?

Right now I have a Django queryset that I want to filter by the result of another query set. Right now I am doing this as (and it works):
field = 'content_object__pk'
values = other_queryset.values_list(field, flat=True)
objects = queryset.filter(pk__in=values)
where the field is the name of a foreign key the pk in queryset. The ORM is smart enough to run the above is one query.
I was trying to simplify this to (ie to filter with the object list themselves rather than having to explicitly say pk):
field = 'content_object'
objects = queryset & other_queryset.values_list(field, flat=True)
but this gives the following error:
AssertionError: Cannot combine queries on two different base models.
What is the right way to do this type of filtering?
You can do the next:
result = MyModel.objects.filter(field__in=other_query)
The results will be the objects in the model where the field is a foreign key to the model in the other_query
you can chain queries in Django...
qs = entry.objects.filter(...).filter(...)