Exclude related objects before Count - django

For example I've got 3 models User, A, B.
class A(models.Model):
creator = models.ForeignKey(
'users.User', on_delete=models.CASCADE, related_name='A_set'
)
class B(models.Model):
user = models.ForeignKey(
'users.User', on_delete=models.CASCADE, related_name='B_set'
)
a_model = models.ForeignKey(
'a.A', on_delete=models.CASCADE, related_name='B_set'
)
I would like to get count of B where user isn't a creator of a_model object.
I've tried query:
`User.objects.prefetch_related('B_set').last().B_set.exclude(a_model__creator=F('user')).count()`
Here is my try with annotation:
User.objects.annotate(b_count=Count('B_set', filter=(~Q(B_set__A__creator=F('user')))))
But I am getting an error:
Cannot resolve keyword 'user' into field.
And then it suggest me fields that relate to User. Also I tried to change user to B_set__user with F() but it didn't help.

With your annotation:
User.objects.annotate(
b_count=Count('B_set', filter=(~Q(B_set__A__creator=F('user'))))
)
your F(..) attribute refers to a hypothetical User.user field, but a User has (probably) no user field.
If you want to refer to the "self" here, you can use F('pk') (or F('id'), given if id is the primary key), so you can write this expression as:
User.objects.annotate(
b_count=Count('B_set', filter=~Q(B_set__a__model__creator=F('pk')))
)
This then results in a query like:
SELECT user.*,
COUNT(
CASE WHEN NOT a.creator_id = user.id AND a.creator_id IS NOT NULL
THEN b.id ELSE NULL END
) AS b_count
FROM user
LEFT OUTER JOIN b ON user.id = b.user_id
LEFT OUTER JOIN a ON b.a_model_id=a.id
GROUP BY user.id

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.

Merging a field in one model to another with ForeignKey, Django Subquery and OuterRef

I have two models, RetailLocation and Transaction, which share a one-to-many relationship respectively. My goal is to annotate RetailLocation with the date of the latest Transaction which corresponds to that RetailLocation.
The model Transaction includes "r_loc_name" which is the exact same string as RetailLocation.name. I believe this field is redundant, and I expect there is a better solution using only the r_loc ForeignKey (and set) in RetailLocation, however I have not figured out how.
Models
class RetailLocation(models.Model):
name = models.CharField(max_length=200, null=True)
class Transaction(models.Model):
date = models.DateTimeField(null=True)
r_loc = models.ForeignKey(RetailLocation, null=True, on_delete=models.SET_NULL)
r_loc_name = models.CharField(max_length=200, null=True) # Likely redundant
Attempted Code
loc_latest = RetailLocation.objects.all().annotate(
latest_txn_time=Subquery(
Transaction.objects.filter(r_loc_name=OuterRef("name"))
.order_by("date")
.values("date")
.last()
)
)
Another Attempt
loc_latest = RetailLocation.objects.all().annotate(
latest_txn_time=Value(
Subquery(
RetailLocation.objects.get(name=OuterRef("name"))
.transaction_set.order_by("date")
.last()
.date
),
DateTimeField(),
)
)
My goal is to to reference RetailLocation.latest_txn_time, which would be the annotated datetime of the latest Transaction referenced to that RetailLocation.
Thank you very much
You can work with the OuterRef on the primary key, and use slicing ([:1]) to obtain the last item, so:
loc_latest = RetailLocation.objects.annotate(
latest_txn_time=Subquery(
Transaction.objects.filter(
r_loc=OuterRef('pk')
).order_by('-date').values('date')[:1]
)
)
The r_loc_name is indeed redundant. For a Transaction object you can work with my_transaction.r_loc.name, and you can filter, etc. with Transaction.objects.filter(r_loc__name='some-name').

The best way to do an efficient filter query in django

models.py file
I am not so good at this aspect in Django. Please can someone help me? I wish to know if there is a more efficient way for the class method already_voted
class Vote(TimeStamped):
voter = models.ForeignKey(get_user_model(), verbose_name=_("Vote"), on_delete=models.CASCADE)
contester = models.ForeignKey(Contester, verbose_name=_("Contester"), on_delete=models.CASCADE,
help_text=("The chosen contester"), related_name="votes")
ip_address = models.GenericIPAddressField(
_("Voter's IP"),
protocol="both",
unpack_ipv4=False,
default="None",
unique=True
)
num_vote = models.PositiveIntegerField(_("Vote"), default=0)
class Meta:
unique_together = ('voter','contester')
verbose_name = _("Vote")
verbose_name_plural = _("Votes")
permissions = (
("vote_multiple_times", "can vote multiple times"),
)
....
....
#classmethod
def already_voted(cls, contester_id, voter_id=None, ip_addr=None):
return cls.objects.filter(contester_id=contester_id).exists() and \
(cls.objects.filter(ip_address=ip_addr).exists() or \
cls.objects.filter(voter_id=voter_id).exists())
The class method may be right, but your model needs one more index:
contester = models.ForeignKey( db_index= True #... )
Notice that:
voter doesn't need index because is on first place on unique_together constraint.
contester needs index because, despite it is on unique_together, doesn't is place on first position of the constraint.
ip_address doesn't need index because has unique constraint.
Also:
unique_together is deprecated and should be a list of tuples (not just a tuple)
Edited
Edited 5 feb 2021 due to OP comment
You can get results in just one hit using Exists but it is less readable, also, I'm not sure if it is more efficient or the best way:
from django.db.models import Exists
q_ip=Vote.objects.filter(ip_address="1")
q_voter=Vote.objects.filter(voter=2)
already_voted=(
Vote
.objects
.filter(contester=3)
.filter(Exists(q_ip)|Exists(q_voter))
.exists())
The underlying sql, you can see this is just one query:
SELECT ( 1 ) AS "a"
FROM "a1_vote"
WHERE ( "a1_vote"."contester" = 3
AND ( EXISTS(SELECT U0."id",
U0."voter",
U0."contester",
U0."ip_address",
U0."num_vote"
FROM "a1_vote" U0
WHERE U0."ip_address" = '1')
OR EXISTS(SELECT U0."id",
U0."voter",
U0."contester",
U0."ip_address",
U0."num_vote"
FROM "a1_vote" U0
WHERE U0."voter" = 2) ) )
LIMIT 1

Django ORM: Reverse relation query multiple model from single models

I am in trouble querying reverse relation, I learned a lot about select_related and prefetch_related, yet I failed to achieve this.
At first see my models:
from django.db import models
import uuid
class Person(models.Model):
alias = models.UUIDField(primary_key=True,default=uuid.uuid4, editable=False, unique=True)
name = models.CharField(max_length=20)
class Appointment(models.Model):
patient = models.ForeignKey(Person, related_name="patient_for_appointment", on_delete=models.CASCADE)
data = models.CharField(max_length=20)
class Sales(models.Model):
customer = models.ForeignKey(Person, related_name="customer_for_sales", on_delete=models.CASCADE)
amount = models.FloatField()
class Prescription(models.Model):
patient = models.ForeignKey(Person, related_name="Patient_for_prescription", on_delete=models.CASCADE)
details = models.CharField(max_length=100)
I am trying to filter Person Model to check if the person has any prescription, sales, and appointment I want to get these all with single query like the person. will filter it with a person alias (primary key)
I can filter it with separate query like
patient_alias = '53fsdfsdf-fdsfds-df-fdf'
queryset = Appointment.objects.filter(
patient__alias=patient_alias
)
But I don't want this, coz, it has a performance issue. I don't want this with separate query.
I want to query only Person model to check if a person has Appointment, prescription or Sales
like Person.objects.filter(alias='a person alias)
Can anyone please help me to achieve this?
Much appreciated
We can select all Persons, except the ones that hav no Appointment, nor a Sales or Prescription with:
Person.objects.exclude(
patient_for_appointment=None,
customer_for_sales=None,
Patient_for_prescription=None
)
This will yield a query that looks like:
SELECT person.alias, person.name
FROM person
WHERE NOT (
person.alias IN (
SELECT U0.alias
FROM person U0
LEFT OUTER JOIN prescription U1 ON U0.alias = U1.patient_id
WHERE U1.id IS NULL
)
AND person.alias IN (
SELECT U0.alias
FROM person U0
LEFT OUTER JOIN sales U1 ON U0.alias = U1.customer_id
WHERE U1.id IS NULL AND U0.alias = person.alias
)
AND person.alias IN (
SELECT U0.alias
FROM person U0
LEFT OUTER JOIN appointment U1
ON U0.alias = U1.patient_id
WHERE U1.id IS NULL AND U0.alias = person.alias
)
)
Or we can make a union, like:
Person.objects.filter(
patient_for_appointment__isnull=False
).union(
Person.objects.filter(customer_for_sales__isnull=False),
Person.objects.filter(Patient_for_prescription__isnull=False)
)
This will result in a query that looks like:
(
SELECT person.alias, person.name
FROM person
INNER JOIN appointment ON person.alias = appointment.patient_id
WHERE appointment.id IS NOT NULL
) UNION (
SELECT person.alias, person.name FROM person
INNER JOIN sales ON person.alias = sales.customer_id
WHERE sales.id IS NOT NULL
) UNION (
SELECT person.alias, person.name
FROM person
INNER JOIN prescription ON person.alias = prescription.patient_id
WHERE prescription.id IS NOT NULL
)

Return value of related object with condition

As for example I've got 3 models: User, Event, Participator
class Event(..):
creator = models.ForeignKey('users.User', on_delete=models.CASCADE, related_name='event_creator_set')
class Participator(..):
status = models.CharField(..)
event = models.ForeignKey('events.Event', on_delete=models.CASCADE, related_name='participators_set')
user = models.ForeignKey('users.User', on_delete=models.CASCADE, related_name='participations_set')
I want to get all user's and also get information about relation with specific event. If user is even't participator -> return status from Participator models, else -> null
Here is my queries:
e = Event.objects.first()
users = User.objects.annotate(is_participationg=Case(When(id__in=e.participators_set.values_list('user__id', flat=True), then=Value(True)), default=Value(False), output_field=BooleanField()))
So I can know whether user is participating in specific event. How can I get user's participating status in then and None in default?
I think you make things too complicated, you know the id of the event, so you can filter like:
from django.db.models import Case, CharField, F, Max, Value, When
User.objects.annotate(
participation_status=Max(Case(
When(participations_set__event=e, then=F('participations_set__status')),
default=Value(None),
output_field=CharField()
))
)
This then results in the query:
SELECT user.*,
MAX(CASE WHEN participator.event_id = 123
THEN participator.status
ELSE NULL END
) AS participation_status
FROM user
LEFT OUTER JOIN participator ON (user.id = participator.user_id)
GROUP BY user.id
(with 123 in reality the primary key of e).
In case the User participated in the event in multiple ways, the lexicographical maximum status will be used.