Suppose there is a structure like this:
PARTICIPATION_STATUSES = (
(0, 'No, thanks'),
(1, 'I may attend'),
(2, 'I\'ll be there'),
)
class Model1(models.Model):
# ...
class Model2(models.Model):
status = models.PositiveIntegerField(
_('participation status'), choices=PARTICIPATION_STATUSES)
field = models.ForeignKey(Model1, related_name='model1_participation')
What I want to do is to annotate each object of Model1 with count of Model2 objects where status equals a specific value (status number is this particular example).
In my pseudo code it would look like:
queryset = Model1.objects.all()
queryset.annotate(declined=Count('model1_participation__status=0'))
queryset.annotate(not_sure=Count('model1_participation__status=1'))
queryset.annotate(accepted=Count('model1_participation__status=2'))
But I can't annotate the queryset in this way as Django doesn't resolve status=<n>.
What is the right way to achieve what I want?
If you are using Django 1.8 or above you can use Conditional Aggregations, these should work for annotate querysets.
from django.db.models import IntegerField, Case, When, Count
queryset = Model1.objects.all()
queryset = queryset.annotate(
declined=Count(
Case(When(model1_participation__status=0, then=1),
output_field=IntegerField())
),
not_sure=Count(
Case(When(model1_participation__status=1, then=1),
output_field=IntegerField())
),
accepted=Count(
Case(When(model1_participation__status=2, then=1),
output_field=IntegerField())
)
)
You can use an Exists Subquery:
from django.db.models.expressions import Exists, ExpressionWrapper, OuterRef, Subquery, Value
from django.db.models.fields import BooleanField
queryset = Model1.objects.all()
queryset.annotate(
declined=ExpressionWrapper(
Exists(Model2.objects.filter(
field=OuterRef('id'),
status=0)),
output_field=BooleanField()))),
not_sure=ExpressionWrapper(
Exists(Model2.objects.filter(
field=OuterRef('id'),
status=1)),
output_field=BooleanField()))),
accepted=ExpressionWrapper(
Exists(Model2.objects.filter(
field=OuterRef('id'),
status=2)),
output_field=BooleanField())))
)
To make it a bit more clear/reusable you can refactor into a function:
def is_status(status_code):
return ExpressionWrapper(
Exists(Model2.objects.filter(
field=OuterRef('id'),
status=status_code)),
output_field=BooleanField())))
Model1.objects.annotate(
declined=is_status(0),
not_sure=is_status(1),
accepted=is_status(2)
)
Related
Basically the problem I have: I need an option or alternative approach to filter on annotated fields on union queryset.
I have the following simplified models setup:
class Course(Model):
groups = ManyToManyField(through=CourseAssignment)
class CourseAssignment(Model):
course = ForeignKey(Course)
group = ForeignKey(Group)
teacher = ForeignKey(Teacher)
class Lesson(Model):
course = ForeignKey(Course, related_name='lessons')
class AssignmentProgress(Model):
lesson = ForeignKey(related_name='progresses')
course_assignment = ForeignKey(CourseAssignment)
student = ForeignKey(Student)
group = ForeignKey(Group)
status = CharField(choices=(
('on_check', 'On check'),
('complete', 'Complete'),
('assigned', 'Assigned'),
))
deadline = DateTimeField()
checked_date = DateTimeField()
I need to display a statistics on assignment progresses grouped by lessons and groups for which courses assigned. Here is a my initial queryset, note that lessons are repeated in final result, the difference is in annotated data:
def annotated_lessons_queryset():
lessons = None
for course_assignment in CourseAssignment.objects.all():
qs = Lesson.objects.filter(
course=course_assignment.course
).annotate(
completed_progresses=Count(
'progresses',
filter=Q(group=course_assignment.group),
output_field=IntegerField()
),
on_check=Exists(
AssignmentProgress.objects.filter(
lesson=OuterRef('id'), group=course_assignment.group, status='on_check'
)
)
)
lessons = qs if lessons is None else lessons.union(qs)
return lessons
I canon use | OR operator here, because it returns only distinct lesson values.
So far this works until I try filter all the lessons with annotated status on_check:
qs = annotated_lessons_queryset().filter(on_check=True)
Which fails with the error:
raise NotSupportedError(
django.db.utils.NotSupportedError: Calling QuerySet.filter() after union() is not supported.
Please, suggest a workaround or another approach to make this queryset filtered.
I haven't pulled this in and tried it out yet, but as the error message states you have to use union() last. This is a bit complicated as "Lessons can be repeated" in this queryset. So I would suggest using a list comprehension to get what you need out.
qs = annotated_lessons_queryset()
filtered = [lesson for lesson in qs if lesson.on_check]
I am new to django and django-rest-framework and I am wondering if it is possible to put conditional if else on annotate(total_sessions=Count()) where if Count() = 0 then total_sessions="null"? I have a project where I need to make that if the total sessions is equal to 0 the output must be null.
I though of using SerializerMethodField() on total_sessions to get the count but this cause multiple SQL queries causing slow API response that is why it is out of the question.
Sample code below for serializers.py and views.py(this is only a sample code as my real codes have multiple querysets).
serializers.py
class ClassStatisticsSerializer(ModelBaseSerializer):
total_sessions = serializers.IntegerField()
class Meta:
model = Class
fields = (
'id',
'batch',
'type,
'total_sessions'
)
views.py
from django.db.models import Count, Q
class ClassStatisticsList(APIView):
condition = {}
if date:
condition = {'classactivity__date':date}
queryset = Class.objects.all().annotate(
total_sessions=Count(
'classactivity',
filter=Q(**condition),
distinct=True
)
)
You are looking for Conditional Expressions that are documented nicely on the Django site.
I haven't tried the following snippet (it's just your code augmented), it's just to give you a starting point:
queryset = Class.objects.all()
.annotate(
total_session_cnt=Count(
'classactivity',
filter=Q(**condition),
distinct=True
)
)
.annotate(
total_sessions=Case(
When(total_session_count=0, then=Value(None, output_field=IntegerField())),
default='total_session_count'
)
)
class MyModel(models.Model):
TRANSACTION_TYPE_CHOICES = (
('p', 'P'),
('c', 'C'),
)
status = models.CharField(max_length=50, choices=TRANSACTION_TYPE_CHOICES, default='c')
user = models.ForeignKey(User, db_index=True, on_delete=models.CASCADE,related_name='user_wallet')
date = models.DateField(auto_now=True)
amount = models.FloatField(null=True, blank=True)
def __unicode__(self):
return str(self.id)
I am a fresher in Python django and have a little knowledge in Django Rest Framework.
I have a model like above and I want to filter the date field by month and get distinct queryset by month.....Is there any default way to do this...
Thanks in advance
you can use TruncMonth with annotations
from django.db.models.functions import TruncMonth
MyModel.objects.annotate(
month=TruncMonth('date')
).filter(month=YOURVALUE).values('month').distinct()
or if you need only filter date by month with distinct you can use __month option
MyModel.objects.filter(date__month=YOURVALUE).distinct()
Older django
you can use extra, example for postgres
MyModel.objects.extra(
select={'month': "EXTRACT(month FROM date)"},
where=["EXTRACT(month FROM date)=%s"],
params=[5]
# CHANGE 5 on you value
).values('month').distinct()
This may help you
MyModel.object.values('col1','col2',...,'date').distinct('date')
OR try this:
from django.db.models.functions import TruncMonth
MyModel.objects
.annotate(month=TruncMonth('date')) # Truncate to month and add to select list
.values('month') # Group By month
.annotate(c=Count('id')) # Select the count of the grouping
.values('month', 'c') # (might be redundant, haven't tested) select month and count
I have a model called LeaveEntry:
class LeaveEntry(models.Model):
date = models.DateField(auto_now=False, auto_now_add=False)
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.PROTECT,
limit_choices_to={'is_active': True},
unique_for_date='date'
)
half_day = models.BooleanField(default=False)
I get a set of LeaveEntries with the filter:
LeaveEntry.objects.filter(
leave_request=self.unapproved_leave
).count()
I would like to get an aggregation called total days, so where a LeaveEntry has half_day=True then it is half a day so 0.5.
What I was thinking based on the django aggregations docs was annotating the days like this:
days = LeaveEntry.objects.annotate(days=<If this half_day is True: 0.5 else 1>)
You can use django's conditional expressions Case and When (only for django 1.8+):
Keeping the order of filter() and annotate() in wind you can count the the number of days left for unapproved leaves like so:
from django.db.models import FloatField, Case, When
# ...
LeaveEntry.objects.filter(
leave_request=self.unapproved_leave # not sure what self relates to
).annotate(
days=Count(Case(
When(half_day=True, then=0.5),
When(half_day=False, then=1),
output_field=FloatField()
)
)
)
I want to annotate a queryset with a related object, if that object exists, or with another field if it doesn't:
#models.py
class MyModel(Model):
f1 = ForeignKey(MyRelated, related_name='f1')
f2 = ForeignKey(MyRelated, related_name='f2', null=True)
#queryset
MyModel.objects.annotate(
f=Case(
When( *something* ,then=F('f2')),
default=F('f1')
)
)
What I need to know is what to put in in place of *something* to check if the foreign key exists or not.
Rubber ducked this one out immediately:
MyModel.objects.annotate(
f=Case(
When(f2__isnull=False ,then=F('f2')),
default=F('f1')
)
)