Division in Django Query - django

Model:
class Vote(models.Model):
thumbs_up = models.ManyToManyField(
settings.AUTH_USER_MODEL, blank=True, related_name='thumbs_up')
thumbs_down = models.ManyToManyField(
settings.AUTH_USER_MODEL, blank=True, related_name='thumbs_down')
View:
qs = Vote.objects.all()
percent_min = request.GET.get('min-rating')
percent_max = request.GET.get('max-rating')
qs = qs.annotate(percent=(Count('thumbs_up')/(Count('thumbs_down')+Count('thumbs_up')))
* 100).filter(percent__gte=percent_min)
qs = qs.annotate(percent=(Count('thumbs_up')/(Count('thumbs_down')+Count('thumbs_up')))
* 100).filter(percent__lte=percent_max)
I also tried this which also didn't work.
qs = qs.annotate(up=Count('thumbs_up', distinct=True), combined=Count('thumbs_up', distinct=True) +
Count('thumbs_down', distinct=True), result=(F('up')/F('combined'))*100).filter(result__gte=percent_min)
I'm attempting to filter by min and max percentages based on user votes (up and down) but I can't seem to get it to work properly.
Using the current code if I, for example, put a maximum percentage of 74% in then it filters out everything rated 100% and leaves the remaining. The opposite happens if I enter 74% as a minimum percentage, it filters everything except those rated 100%.
Currently no 0 rated entries as I have to tackle the divide by 0 issue next.
Any insights would be greatly appreciated.

So I came up with this which seems to be working:
qs = qs.annotate(meh=Count('thumbs_meh', distinct=True), up=Count('thumbs_up', distinct=True), combined=Count('thumbs_up', distinct=True) +
Count('thumbs_down', distinct=True) + Count('thumbs_meh', distinct=True), result=Case(When(combined=0, then=0), default=((F('up')+(F('meh')/2))/(1.0*F('combined')))*100)).filter(result__gte=rating_min)
I added another model field for 'meh' votes hence the addition to the query.

You can do something similar to
Vote.objects.filter(percentage__range=(min_perct, max_perct))
Although you need to generate a separate field for this method to work. Try adding:
class Vote
# ...
percentage = models.DecimalField(default=0, max_digits=5, decimal_places=2)
# Remember to update this field after every update!
v = Vote()
v.percentage = (thumbs_up/(thumbs_up + thumbs_down))

Related

Django Annotation Count with Subquery & OuterRef

I'm trying to create a high score statistic table/list for a quiz, where the table/list is supposed to be showing the percentage of (or total) correct guesses on a person which was to be guessed on. To elaborate further, these are the models which are used.
The Quiz model:
class Quiz(models.Model):
participants = models.ManyToManyField(
User,
through="Participant",
through_fields=("quiz", "correct_user"),
blank=True,
related_name="related_quiz",
)
fake_users = models.ManyToManyField(User, related_name="quiz_fakes")
user_quizzed = models.ForeignKey(
User, related_name="user_taking_quiz", on_delete=models.CASCADE, null=True
)
time_started = models.DateTimeField(default=timezone.now)
time_end = models.DateTimeField(blank=True, null=True)
final_score = models.IntegerField(blank=True, default=0)
This model does also have some properties; I deem them to be unrelated to the problem at hand.
The Participant model:
class Participant(models.Model): # QuizAnswer FK -> QUIZ
guessed_user = models.ForeignKey(
User, on_delete=models.CASCADE, related_name="clicked_in_quiz", null=True
)
correct_user = models.ForeignKey(
User, on_delete=models.CASCADE, related_name="solution_in_quiz", null=True
)
quiz = models.ForeignKey(
Quiz, on_delete=models.CASCADE, related_name="participants_in_quiz"
)
#property
def correct(self):
return self.guessed_user == self.correct_user
To iterate through what I am trying to do, I'll try to explain how I'm thinking this should work:
For a User in User.objects.all(), find all participant objects where the user.id equals correct_user(from participant model)
For each participantobject, evaluate if correct_user==guessed_user
Sum each participant object where the above comparison is True for the User, represented by a field sum_of_correct_guesses
Return a queryset including all users with parameters [User, sum_of_correct_guesses]
^Now ideally this should be percentage_of_correct_guesses, but that is an afterthought which should be easy enough to change by doing sum_of_correct_guesses / sum n times of that person being a guess.
Now I've even made some pseudocode for a single person to illustrate to myself roughly how it should work using python arithmetics
# PYTHON PSEUDO QUERY ---------------------
person = get_object_or_404(User, pk=3) # Example-person
y = Participant.objects.filter(
correct_user=person
) # Find participant-objects where person is used as guess
y_corr = [] # empty list to act as "queryset" in for-loop
for el in y: # for each participant object
if el.correct: # if correct_user == guessed_user
y_corr.append(el) # add to queryset
y_percentage_corr = len(y_corr) / len(y) # do arithmetic division
print("Percentage correct: ", y_percentage_corr) # debug-display
# ---------------------------------------------
What I've tried (with no success so far), is to use an ExtensionWrapper with Count() and Q object:
percentage_correct_guesses = ExpressionWrapper(
Count("pk", filter=Q(clicked_in_quiz=F("id")), distinct=True)
/ Count("solution_in_quiz"),
output_field=fields.DecimalField())
all_users = (
User.objects.all().annotate(score=percentage_correct_guesses).order_by("score"))
Any help or directions to resources on how to do this is greatly appreciated :))
I found an answer while looking around for related problems:
Django 1.11 Annotating a Subquery Aggregate
What I've done is:
Create a filter with an OuterRef() which points to a User and checks if Useris the same as correct_person and also a comparison between guessed_person and correct_person, outputs a value correct_user in a queryset for all elements which the filter accepts.
Do an annotated count for how many occurrences there are of a correct_user in the filtered queryset.
Annotate User based on the annotated-count, this is the annotation that really drives the whole operation. Notice how OuterRef() and Subquery are used to tell the filter which user is supposed to be correct_user.
Below is the code snippet which I made it work with, it looks very similar to the answer-post in the above linked question:
from django.db.models import Count, OuterRef, Subquery, F, Q
crit1 = Q(correct_user=OuterRef('pk'))
crit2 = Q(correct_user=F('guessed_user'))
compare_participants = Participant.objects.filter(crit1 & crit2).order_by().values('correct_user')
count_occurrences = compare_participants.annotate(c=Count('*')).values('c')
most_correctly_guessed_on = (
User.objects.annotate(correct_clicks=Subquery(count_occurrences))
.values('first_name', 'correct_clicks')
.order_by('-correct_clicks')
)
return most_correctly_guessed_on
This works wonderfully, thanks to Oli.

Django: Filtering a related field by date yields unwanted results

models:
class Vehicle(models.Model):
licence_plate = models.CharField(max_length=16)
class WorkTime(models.Model):
work_start = models.DateTimeField()
work_end = models.DateTimeField()
vehicle = models.ForeignKey(Vehicle, on_delete=models.SET_NULL, related_name="work_times")
However when I try to filter those working times using:
qs = Vehicle.objects.filter(
work_times__work_start__date__gte="YYYY-MM-DD",
work_times__work_end__date__lte="YYYY-MM-DD").distinct()
I get results that do not fit the timeframe given. Most commonly when the work_end fits to something, it returns everything from WorkTime
What I would like to have:
for vehicle in qs:
for work_time in vehicle.work_times:
print(vehicle, work_time.work_start, work_time.work_end)
The filter has no effect on the .work_times from the Vehicles, it only will ensure that the Vehicles in the qs will contain at least one WorkTime in the given range.
You can work with a Prefetch object [Django-doc] to allow filtering efficiently on a related manager:
from django.db.models import Prefetch
qs = Vehicle.objects.prefetch_related(
Prefetch(
'work_times',
WorkTime.objects.filter(
work_start__date__range=('2021-03-01', '2021-03-12')
),
to_attr='filtered_work_times'
)
)
and then you can work with:
for vehicle in qs:
for work_time in vehicle.filtered_work_times:
print(vehicle, work_time.work_start, work_time.work_end)

Get the top n rows of each day from same table in Django

I am sure this is not a novel/new problem. I really tried to look into other solutions. However, I could not find how to solve this.
I have a model like
class Deal(models.Model):
title = models.CharField(max_length=1500)
viewCounter = models.SmallIntegerField(default=0)
thumbsUpCounter = models.SmallIntegerField(default=0)
createDateTime = models.DateTimeField(auto_now_add=True)
owner = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, null=False, related_name='deal_owner',
editable=False)
Now I want to get top 10 (or less) deals ordering by thumbsUpCounter and viewCounter of every day. I tried to look into the Subquery and Outerref. However, I could not figure out how can I get the right results.
** I am using MySQL.
Thanks in advance.
try
from django.db.models.functions import TruncDate
query = Deal.objects.annotate(date=TruncDate('createDateTime'))\ # extract date
.values('date')\ # group by date
.order_by('-thumbsUpCounter')\ # order by
[:10] # slice first 10

Django: Aggregate (sum) of a field on two different set of data in a single query

I am using Django 1.6.
My Model looks something like:
Class Transaction(models.Model):
type = models.CharField(max_length=255, db_index=True)
amount = models.DecimalField(decimal_places=2, max_digits=10, default=0.00)
I have few transactions, few of which are credit and other are debit (determined by type column). I need to check the balance of all transaction i.e., (debit - credit)
Currently, I could do that using 2 queries as below:
debit_amount=Transaction.objects.fitler(type='D').aggregate(debit_amount=Sum('amount'))['debit_amount']
credit_amount=Transaction.objects.fitler(type='C').aggregate(credit_amount=Sum('amount'))['credit_amount']
balance = debit_amount - credit_amount
I am looking something like:
Transaction.objects.aggregate(credit=Sum('amount', filter=Q(type='C')), debit=Sum('amount', filter=Q(type='D')))
You can use conditional expression
from django.db.models import *
result = Transaction.objects.aggregate(
credit=Sum(Case(
When(Q(tye='C'), then=F('amount')),
output_field=IntegerField(),
default=0
)),
debit=Sum(Case(
When(Q(tye='D'), then=F('amount')),
output_field=IntegerField(),
default=0
)),
)
balance = result['debit'] - result['credit']
This should be possible in django 2.0 (https://docs.djangoproject.com/en/2.0/ref/models/conditional-expressions/#case)
totals = Transaction.objects.aggregate(
credit=Sum('amount', filter=Q(type='C')),
debit=Sum('amount', filter=Q(type='D'))
)
total = totals.credits - totals.debit

Ordering a query by aggregate sum in Django, but not getting result as expected

I am trying to create a Django site that shows a list of products, and orders them by the total score of votes they have received. 'Product' is one class, and 'Vote' is another, with a field called score which can be 1, 0, or -1. I want Products to be ordered according to the sum of the score for votes associated with each particular product.
Here is my models.py:
class Product(models.Model):
Content = models.TextField()
creation_date = models.DateTimeField( default=datetime.now )
total_votes = models.IntegerField( default=0 )
def __unicode__(self):
return self.content
class Vote(models.Model):
voter = models.ForeignKey( User )
product = models.ForeignKey( Product )
score = models.IntegerField( default=0, choices=VOTE_SCORE_CHOICES)
def __unicode__(self):
return '%s - %s : %s' % (self.product.id, self.voter, self.score)
And here's my views.py:
def show_products( request):
product_list = Product.objects.all()
# set the field 'total_votes' in prduct to the sum of votes for each sentence
for p in product_list:
try:
v = Vote.objects.filter( product = p ).aggregate(Sum('score'))['score__sum']
except IndexError:
v = 0
p.total_votes = v
p.save()
# get the product that has the highest vote score
try:
top_product = product_list.order_by('-total_votes')[0].content
except IndexError:
top_product = 'no product'
# reorder product_list according to total_votes
product_list = product_list.order_by('total_votes')
return render_to_response('product/product_detail.html',
{'product_list': product_list,
'top_produce': top_product,}, context_instance=RequestContext(request))
So you see I'm getting the sum of the score for votes associated with each product, then assigning this number to each product's 'total_votes' field, and then reordering the product list according to 'total_votes'.
However, the results are not as expected, and the products get ordered separately from the vote scores. Can somebody tell me what's wrong with the code here? And also, is this the right way to be approaching this issue?
Thank you
This seems an unnecessarily complicated way to do this. You're aggregating votes for every product separately, saving the result in the product, then ordering by those vote sums.
Instead of that, seems like you should be doing it all in one go:
product_list = Product.objects.annotate(total_votes=Sum('vote__score')).order_by('total_votes')
(I'm presuming your initial reference to sentence and b are meant to refer to product and p respectively - otherwise you'll need to give more details about what these actually are.)