Confusion with Queryset.annotate() - django

I have two models:
class Property(models.Model):
# code here...
class AccommodationType(models.Model):
property = models.ForeignKey(Property, related_name='accommodation_types')
# rest of code here...
What I'm trying to to is to annotate Property's queryset with count of related AccommodationType's and filter it by the value of this count. So here is my code:
qs = Property.objects.all()
qs.annotate(acc_types_count=Count('accommodation_types'))
filtered = qs.filter(acc_types_count=1)
and here I got the error:
django.core.exceptions.FieldError: Cannot resolve keyword 'acc_types_count' into field. Choices are: # ...rest of the fields
Where I am wrong?

annotate, like filter, doesn't mutate the queryset but returns a new one. You need to reassign that to qs:
qs.annotate(acc_types_count=Count('accommodation_types'))
Or combine it with the original query:
qs = Property.objects.all().annotate(acc_types_count=Count('accommodation_types'))

Related

Django: Change Queryset of already defined Prefetch object

Context
I really want to, but I don't understand how I can limit an already existing Prefetch object
Models
class MyUser(AbstractUser):
pass
class Absence(Model):
employee = ForeignKey(MyUser, related_name='absences', on_delete=PROTECT)
start_date = DateField()
end_date = DateField()
View
class UserAbsencesListAPIView(ListAPIView):
queryset = MyUser.objects.order_by('first_name')
serializer_class = serializers.UserWithAbsencesSerializer
filterset_class = filters.UserAbsencesFilterSet
Filter
class UserAbsencesFilterSet(FilterSet):
first_name = CharFilter(lookup_expr='icontains', field_name='first_name')
from_ = DateFilter(method='filter_from', distinct=True)
to = DateFilter(method='filter_to', distinct=True)
What do I need
With the Request there are two arguments from_ and to. I should return Users with their Absences, which (Absences) are bounded by from_ and/or to intervals. It's very simple for a single argument, i can limit the set using Prefetch object:
def filter_from(self, queryset, name, value):
return queryset.prefetch_related(
Prefetch(
'absences',
Absence.objects.filter(Q(start_date__gte=value) | Q(start_date__lte=value, end_date__gte=value)),
)
)
Similarly for to.
But what if I want to get a limit by two arguments at once?
When the from_ attribute is requested - 'filter_from' method is executed; for the to argument, another method filter_to is executed.
I can't use prefetch_related twice, I get an exception ValueError: 'absences' lookup was already seen with a different queryset. You may need to adjust the ordering of your lookups..
I've tried using to_attr, but it looks like I can't access it in an un-evaluated queryset.
I know that I can find the first defined Prefetch in the _prefetch_related_lookups attribute of queryset, but is there any way to apply an additional filter to it or replace it with another Prefetch object so that I can end up with a query similar to:
queryset.prefetch_related(
Prefetch(
'absences',
Absence.objects.filter(
Q(Q(start_date__gte=from_) | Q(start_date__lte=from_, end_date__gte=from_))
& Q(Q(end_date__lte=to) | Q(start_date__lte=to, end_date__gte=to))
),
)
)
django-filter seems to have its own built-in filter for range queries:
More info here and here
So probably just easier to use that instead:
def filter_date_range(self, queryset, name, value):
if self.lookup_expr = "range":
#return queryset with specific prefetch
if self.lookup_expr = "lte":
#return queryset with specific prefetch
if self.lookup_expr = "gte":
#return queryset with specific prefetch
I haven't tested this and you may need to play around with the unpacking of value but it should get you most of the way there.

How to query on OneToOne relation of a related_name object in django

I have this model:
class ProgramRequirement(Model):
program = OneToOneField(Program, related_name='program_requirement')
prereq_program = ForeignKey(Program, related_name='prereq_program_requirement')
is_english_required = BooleanField()
and this model
class Program(Model):
field_1 = ...
field_3 = ...
I need to write a query that would return the primary key of the programs of which is_english_required of the prereq_program is True.
I tried this but it seems to be a wrong query:
ProgramRequirement.objects.filter(prereq_program__prereq_program_requirement__is_english_required =True).values_list('program__pk', flat=True)
However, it is not returning the correct result.
I am not sure if it is what I want but I am also thinking of this:
Program.objects.filter(prereq_program_requirement__is_english_required =True).values_lis('pk', flat=True)
Any idea of how to retrieve do the abovementioned result?
You might try:
ProgramRequirement.objects.filter(prereq_program__programrequirement__is_english_required = True).values('pk')

Django QuertySet.annotate() received non-expression - how to add a derived field based on model field?

First time with Django. Trying to add an annotation to queryset:
class EnrollmentManager(models.Manager.from_queryset(EnrollmentCustomQuerySet)):
COURSE_DURATION = datetime.timedelta(days=183)
def get_queryset(self):
"""Overrides the models.Manager method"""
lookback = make_aware(datetime.datetime.today() - self.COURSE_DURATION)
qs = super(EnrollmentManager, self).get_queryset().annotate( \
is_expired=(Value(True)), output_field=models.BooleanField())
return qs
At the moment I am just trying to add an extra 'calculated' field on the returned queryset, which is hard-coded to True and the attribute/field should be called is_expired.
If I can get that to work, then Value(True) needs to be a derived value based on this expression:
F('enrolled') < lookback
But since 'enrolled' is a database field and lookback is calculated, how will I be able to do that?
Note
I tried this, which executes without throwing the error:
qs = super(EnrollmentManager, self).get_queryset().annotate( \
is_expired=(Value(True, output_field=models.BooleanField())))
and in the shell I can see it:
Enrollment.objects.all()[0].is_expired -> returns True
and I can add it to the serializer:
class EnrollmentSerializer(serializers.ModelSerializer):
is_active = serializers.SerializerMethodField()
is_current = serializers.SerializerMethodField()
is_expired = serializers.SerializerMethodField()
COURSE_DURATION = datetime.timedelta(days=183)
class Meta:
model = Enrollment
fields = ('id', 'is_active', 'is_current', 'is_expired')
def get_is_expired(self, obj):
return obj.is_expired
So it is possible...but how can I replace my hard-coded 'True" with a calculation?
UPDATE
Reading the documentation, it states:
"Annotates each object in the QuerySet with the provided list of query expressions. An expression may be a simple value, a reference to a field on the model (or any related models), or an aggregate expression (averages, sums, etc.) that has been computed over the objects that are related to the objects in the QuerySet."
A simple value - so, not a simple COMPUTED value then?
That makes me think this is not possible...
It seems like a pretty good use-case for a Case expression. I suggest getting as familiar as you can with these expression tools, they're very helpful!
I haven't tested this, but it should work. I'm assuming enrolled is a tz-aware datetime for when they first enrolled...
from django.db.models import Case, When, Value
def get_queryset(self):
"""Overrides the models.Manager method"""
lookback = make_aware(datetime.datetime.today() - self.COURSE_DURATION)
qs = super(EnrollmentManager, self).get_queryset().annotate(
is_expired=Case(
When(
enrolled__lt=lookback,
then=Value(True)
),
default=Value(False),
output_field=models.BooleanField()
)
)
You also don't have to pre-calculate the lookback variable. Check out ExpressionWrappers and this StackOverflow answer that addresses this.
ExpressionWrapper(
TruncDate(F('date1')) + datetime.timedelta(days=365),
output_field=DateField(),
)

Using distinct on annotations

I am trying to get a distinct list of items. The db has a created field which is datetime and I need it as a date for my query. So I added an annotation. The problem is that distinct won't work on the annotation...
distinct_failed_recharges = recharges.filter(
status=FAILED
).annotate(
created_date=TruncDate('created')
).distinct(
'created_date', 'sim', 'product_type', 'failure_reason'
).values_list('id', flat=True)
This is the error that I get:
django.core.exceptions.FieldError: Cannot resolve keyword 'created_date' into field
I get the same error in django 1.11 doing:
qs = queryset.annotate(day=TruncDay('date')).distinct('day')
ids = list(qs.values_list('id', flat=True))
results with this error:
FieldError: Cannot resolve keyword 'day' into field.
This is very weird since I try to evaluate 'id'...
The only workaround that I've found is:
qs = queryset.annotate(day=TruncDay('date')).distinct('day')
objects_list = list(qs)
ids = [object.id for object in objects_list]
This is very inefficient, but hopefully my list is not too long...

How to get the related field value of item in the queryset inside for loop

Supposed that we have a model Patient and Diagnosis.
class Patient(models.Model):
name = models.CharField()
class Diagnosis(models.Model):
patient = models.ForeignKey(
Patient,
related_name='diagnoses',
on_delete=models.CASCADE
)
is_active = models.BooleanField()
In my views.py i was able to filter Patient whose diagnosis is_active=True with this code.
# returns queryset whose patient has active diagnosis.
queryset = Patient.objects.filter(
diagnoses__is_active = True
)
But i cannot get the value using for loop with this. I added comment with the errors im getting.
for qs in queryset:
# 'RelatedManager' object has no attribute 'is_active'
print qs.diagnoses.is_active
# 'Patient' object has no attribute 'diagnoses__is_active'
print qs.diagnoses__is_active
# There is no error from this code but
# i got the 'diagnoses.Diagnoses.None' in return
print qs.diagnoses
How can it possibly be when i was able to filter the queryset in the first place?
Each Patient object will have a collection of Diagnosis objects associated with it.
When you do for qs in queryset:, qs becomes a Patient.
And then, qs.diagnoses is a RelatedManager that can be used to retrieve all the associated Diagnosis objects, but you need to do:
qs.diagnoses.all()
Or maybe
qs.diagnoses.filter(is_active=True) if you are only interested in the actives ones.
Because each qs has many diagnoses, ie, qs.diagnoses it's a new queryset. You need to use a iterate do get the diagnoses . Try this:
for diagnose in qs.diagnoses.all():
print diagnose.is_active