Django annotate on boolean Field - django

class Forecast(Model):
id = UUID()
type = StringField()
approved = BooleanField()
I want to group on the field type by applying 'logical and' on approved field. Suppose the annotated field is all_approved. all_approved should be True if all the items with that type is True, and false if atleast one is False.
So finally in my queryset i want to have two fields type, all_approved.
How can i achieve this?
I tried something based on this answer, but couldn't get anything.
EDIT:
when i tried whats given in that answer, its not doing 'logical and'. Instead for each type it just give two items, one with all_approved as True, another with all_approved as False. I want a single item for each type.
Also i don't understand why that answer should work. Where is it specified if while grouping it should do 'logical and' or 'logical or'.

other solution: you can try compare all approved with approved=True
from django.db.models import Count, Case, When, BooleanField
Forecast.objects.values(
'type'
).annotate(
cnt_app=Count(Case(When(approved=True, then=1)))
).annotate(
all_approved=Case(
When(cnt_app=Count('approved'), then=True),
default=False,
output_field=BooleanField()
)
).values('type', 'all_approved')
where
Count(Case(When(approved=True, then=1))) gives us count of the approved with status True for the type,
Count('approved') gives us total count of the all for the type,
and if the values is equal then all_approved is True other way False

You can use a subquery to flip all_approved to False for types that have at least one False value:
from django.db.models import BooleanField
from django.db.models.expressions import Case, When
(Forecast.objects
.annotate(all_approved=Case(
When(type__in=Forecast.objects.filter(approved=False).values('type'),
then=False),
default=True,
output_field=BooleanField()
))
.values('type', 'all_approved')
.distinct()
)
The question you've linked is a bit different because it relates to a one-to-many relationship between two models that are joined automatically by Django.
Here you have just one model, which you'd have to join with itself to use the same solution. Since Django only supports joins defined by relationships, you need the subquery as a workaround.

Related

Annotating a FilteredRelation on a ManyToManyField returns nothing

Models
class User(AbstractUser):
pass
class Report(Model):
user = ForeignKey (
"User",
related_name="reports"
)
shared_doctors = ManyToManyField (
"User",
symmetrical = False,
related_name="shared_reports"
)
I have more fields on the models, but I have omitted them in the interest of shortening the problem.
Query
User.objects.annotate(
shared_reports_user = FilteredRelation(
'shared_reports',
condition = Q(shared_reports__user=user)
)
).annotate(
shared_reports_user_count = Count('shared_reports_user')
)
I have brought down the query to the base level which is giving unexpected results. user in the first annotate is an instance of User.
The Query runs successfully, but the resulting QuerySet has no shared_reports_user key, though it has a shared_reports_user_count key, which has a value of 0.
As far as I understood from the documentation, FilteredRelation takes a field present on the Model that it will filter on, and a named argument, condition, that takes a Q object as a condition to be applied to the existing field value. This leads me to believe that the first annotation should return a QuerySet annotated with a subset of shared_reports satisfying the condition specified in the Q object.
I'm sure my database contains values satisfying the Q object (I have manually inspected the data), and even in case there is no output, I would have expected an empty QuerySet against the key shared_reports_user.
Am I missing something?
You can use Conditional aggregation in Django 2.0 like this:
User.objects.annotate(
shared_reports_user_count=Count('shared_reports', filter=Q(shared_reports__user_id=user.id))

How do I add an annotation with Django annotate on a QuerySet?

I have a model class with a status field, which might have several alternatives, say:
class MyModel(models.Model):
status_choices = (
('status1', 'status1'),
('status2', 'status2'),
('status3', 'status3'),
('status4', 'status4'),
)
status = models.CharField(choices=status_choices)
Then I want to annotate the instances with an active field which might be either True or False. The instance is active when status is IN [status1, status2].
Django version is 1.11.
We can do this with a Case expression [Django-doc]:
from django.db.models import BooleanField, Case, Value, When
MyModel.objects.annotate(
active=Case(
When(status__in=['status1', 'status2'], then=Value(True))
default=Value(False),
output_field=BooleanField()
)
)
Note that the list of the status__in condition should contain the keys of the choices (so the left item of the tuples, not the right one).

Where should I specify the output_field kwarg in this complex query?

I wrote a little survey app. I have a query that causes an exception upon upgrade to Django 2.0:
Expression contains mixed types. You must set output_field.
Here's a few relationships necessary to understand the query (where --fk--> indicats a foreign key):
response --fk--> question --fk--> survey
response --fk--> person
Here's my query:
answered_surveys = SurveyResponse.objects.all()\
.values('fk_question__fk_survey__hash', 'fk_question__fk_survey__name')\
.annotate(
nb_respondants = Count('fk_person', distinct=True),
survey_name = F('fk_question__fk_survey__name'),
survey_hash = F('fk_question__fk_survey__hash')
)\
.annotate(
completion=100*Count('id')/
(F('nb_respondants')*F('fk_question__fk_survey__nb_questions_per_survey'))
)
It's fine up until the last annotate, but I don't know where to add the output_field kwarg since I only have F and Count models. Count outputs an IntegerField by definition, F complains if I try to add that kwarg.
How do I fix this?
Thanks to #Gahan's comment, I discovered I need to use an ExpressionWrapper to perform arithmetic on F objects within an annotation. Docs here.
The last part hence becomes:
.annotate(
completion=ExpressionWrapper(
100*Count('id')\
(F('nb_respondants')*F('fk_question__fk_survey__nb_questions_per_survey')),
output_field=FloatField()
)

Annotate queryset with whether matching related object exists

I have two models with an explicit many-to-many relationship: a thing, auth.user, and a "favorite" model connecting the two. I want to be able to order my "thing"s by whether or not they are favorited by a particular user. In Sqlite3, the best query i've come up with is (roughly) this:
select
*, max(u.name = "john cleese") as favorited
from thing as t
join favorite as f on f.thing_id = t.id
join user as u on f.user_id = u.id
group by t.id
order by favorited desc
;
The thing tripping me up in my sql-to-django translation is the max(u.name = "john cleese") bit. As far as I can tell, Django has support for arithmatic but not equality. The closest I can come is a case statement that doesn't properly group the output rows:
Thing.objects.annotate(favorited=Case(
When(favorites__user=john_cleese, then=Value(True)),
default=Value(False),
output_field=BooleanField()
))
The other direction I've tried is to use RawSQL:
Thing.objects.annotate(favorited=RawSQL('"auth_user"."username" = "%s"', ["john cleese"]))
However, this won't work, because (as far as I'm aware) there's no way to explicitly join the favorite and auth_user tables I need.
Is there something I'm missing?
This will achieve what you (or anyone else googling there way here) wants to do:
Thing.objects.annotate(
favorited=Count(Case(
When(
favorites__user=john_cleese,
then=1
),
default=0,
output_field=BooleanField(),
)),
)
From what I read in a related ticket, you can use subquery with the Exists query expression.
Exists is a Subquery subclass that uses an SQL EXISTS statement. In many cases it will perform better than a subquery since the database is able to stop evaluation of the subquery when a first matching row is found.
Assuming the middle model in your case of ManyToMany is called Favorite
from django.db.models import Exists, OuterRef
is_favorited_subquery = Favorite.objects.filter(
thing_id = OuterRef('pk')
)
Thing.objects.annotate(favorited=Exists(is_favorited_subquery))
Then you can order by favorited attribute of the query.
I'm not exactly sure what you're trying to achieve, but I would start it like this way.
from django.db import models
from django.contrib.auth.models import User
class MyUser(models.Model):
person = models.OneToOneField(User)
class Thing(models.Model):
thingname = models.CharField(max_length=10)
favorited_by = models.ManyToManyField(MyUser)
And in your view:
qs = MyUser.objects.get(id=pk_of_user_john_reese).thing_set.all()
Will give you all Thing objects of the given user.
You should have a look in the Django Docs for ManyToMany
I'm using Django for some years now in several smaller and even bigger Projects, but I have never used the RawSQL features. Most times I thought about it, I have had a mistake in my model design.

Annotate filtering -- sum only some of related objects' fields

Let's say there's an Author and he has Books. In order to fetch authors together with the number of written pages, the following can be done:
Author.objects.annotate(total_pages=Sum('book__pages'))
But what if I wanted to sum pages of sci-fi and fantasy books separately? I'd like to end up with an Author, that has total_pages_books_scifi_pages and total_pages_books_fantasy_pages properties.
I know I can do following:
Author.objects.filter(book__category='scifi').annotate(total_pages_books_scifi_pages=Sum('book__pages'))
Author.objects.filter(book__category='fantasy').annotate(total_pages_books_fantasy_pages=Sum('book__pages'))
But how do it in one queryset?
from django.db.models import IntegerField, F, Case, When, Sum
categories = ['scifi', 'fantasy']
annotations = {}
for category in categories:
annotation_name = 'total_pages_books_{}'.format(category)
case = Case(
When(book__category=category, then=F('book__pages')),
default=0,
output_field=IntegerField()
)
annotations[annotation_name] = Sum(case)
Author.objects.filter(
book__category__in=categories
).annotate(
**annotations
)
Try:
Author.objects.values("book__category").annotate(total_pages=Sum('book__pages'))
From Django docs:
https://docs.djangoproject.com/en/1.10/topics/db/aggregation/#values:
values()
Ordinarily, annotations are generated on a per-object basis - an annotated QuerySet will return one result for each object in the original QuerySet. However, when a values() clause is used to constrain the columns that are returned in the result set, the method for evaluating annotations is slightly different. Instead of returning an annotated result for each result in the original QuerySet, the original results are grouped according to the unique combinations of the fields specified in the values() clause. An annotation is then provided for each unique group; the annotation is computed over all members of the group.