Return value of related object with condition - django

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.

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: Check if record exists in two different states

I am using two models to build a chat system between users:
class Chat(models.Model):
created = models.DateTimeField(auto_now_add=True)
class Participant(models.Model):
chat = models.ForeignKey(Chat, on_delete=models.CASCADE, related_name='participants')
sender = models.ForeignKey(CustomUser, on_delete=models.CASCADE)
receiver = models.ForeignKey(CustomUser, on_delete=models.CASCADE)
One record in the participants model represents the ability to send messages from the user sender to the user receiver.
Thus, for a valid private chat between A and B, two records will exist, one with A as sender and B as receiver and vice versa.
Since one user will always be the one starting the chat but the first participant record could be with A as sender or B as sender, I need to know if there's a clean and cheap way to check if both records exist when a user tries to initiate a chat, and return the chat id if it exists.
How do I search for the existence of records (sender=A, receiver=B) and (sender=B, receiver=A) in the same query?
You can use Q objects to create complex queries including matching on one condition OR another
query = Participant.objects.filter(Q(sender=A, receiver=B) | Q(sender=B, receiver=A))
query.count() == 2 # If you want to check that 2 records exist
| in this case creates a filter with an "OR"
You can make use of two JOINs here, like:
Chat.objects.filter(
participants__sender=user_a,
participants__receiver=user_b
).filter(
participants__sender=user_b,
participants__receiver=user_a
)
This will result in a query like:
SELECT chat.id, chat.created
FROM chat
INNER JOIN participant ON chat.id = participant.chat_id
INNER JOIN participant T5 ON chat.id = T5.chat_id
WHERE participant.receiver_id = user_b AND participant.sender_id = user_a
AND T5.receiver_id = user_a AND T5.sender_id = user_b
It will thus return all the Chat objects for which two such Participant objects exist.
The above is not ideal however, since we make two JOINs. In case there is a unique_together constraint on the participants, as in:
class Participant(models.Model):
chat = models.ForeignKey(Chat, on_delete=models.CASCADE, related_name='participants')
sender = models.ForeignKey(CustomUser, on_delete=models.CASCADE)
receiver = models.ForeignKey(CustomUser, on_delete=models.CASCADE)
class Meta:
unique_together = ['sender', 'receiver']
We can just count the number of Participant objects, like:
from django.db.models import Count, Q
Chat.objects.filter(
Q(participants__sender=user_a, participants__receiver=user_b) |
Q(participants__sender=user_b, participants__receiver=user_a)
).annotate(
nparticipants=Count('participants')
).get(
nparticipants=2
)
This will use the following query:
SELECT chat.id, chat.created, COUNT(participant.id) AS nparticipants
FROM chat
INNER JOIN participant ON chat.id = participant.chat_id
WHERE (participant.receiver_id = user_b AND participant.sender_id = user_a)
OR (participant.receiver_id = user_a AND participant.sender_id = user_b)
GROUP BY chat.id
HAVING COUNT(participant.id) = 2
We can use .get(..) here, since due to the unique_together constraint, it is guaranteed that there is at most one Chat object for which this will exist. We can thus then handle the situation with a Chat.DoesNotExist exception.
I am however not really convinced that the above modeling is ideal. First of all the number of records will scale quadratic with the number of participants: for three participants, there are six records. Furthermore a Chat is probably conceptually speaking not "directional": there is no sender and receiver, there are two or more peers that share information.

How can I filter a Django queryset by the latest of a related model?

Imagine I have the following 2 models in a contrived example:
class User(models.Model):
name = models.CharField()
class Login(models.Model):
user = models.ForeignKey(User, related_name='logins')
success = models.BooleanField()
datetime = models.DateTimeField()
class Meta:
get_latest_by = 'datetime'
How can I get a queryset of Users, which only contains users whose last login was not successful.
I know the following does not work, but it illustrates what I want to get:
User.objects.filter(login__latest__success=False)
I'm guessing I can do it with Q objects, and/or Case When, and/or some other form of annotation and filtering, but I can't suss it out.
We can use a Subquery here:
from django.db.models import OuterRef, Subquery
latest_login = Subquery(Login.objects.filter(
user=OuterRef('pk')
).order_by('-datetime').values('success')[:1])
User.objects.annotate(
latest_login=latest_login
).filter(latest_login=False)
This will generate a query that looks like:
SELECT auth_user.*, (
SELECT U0.success
FROM login U0
WHERE U0.user_id = auth_user.id
ORDER BY U0.datetime DESC
LIMIT 1
) AS latest_login
FROM auth_user
WHERE (
SELECT U0.success
FROM login U0
WHERE U0.user_id = auth_user.id
ORDER BY U0.datetime
DESC LIMIT 1
) = False
So the outcome of the Subquery is the success of the latest Login object, and if that is False, we add the related User to the QuerySet.
You can first annotate the max dates, and then filter based on success and the max date using F expressions:
User.objects.annotate(max_date=Max('logins__datetime'))\
.filter(logins__datetime=F('max_date'), logins__success=False)
for check bool use success=False and for get latest use latest()
your filter has been look this:
User.objects.filter(success=False).latest()

Exclude related objects before Count

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

Annotate django query if filtered row exists in second table

I have two tables (similar to the ones below):
class Piece(models.Model):
cost = models.IntegerField(default=50)
piece = models.CharField(max_length=256)
class User_Piece (models.Model):
user = models.ForeignKey(User)
piece = models.ForeignKey(Piece)
I want to do a query that returns all items in Piece, but annotates each row with whether or not the logged in user owns that piece (so there exists a row in User_Piece where user is the logged in user).
I tried:
pieces = Piece.objects.annotate(owned=Count('user_piece__id'))
But it puts a count > 0 for any piece that is owned by any user. I'm not sure where/how I put in the condition that the user_piece must have the specified user I want. If I filter on user__piece__user=user, then I don't get all the rows from Piece, only those that are owned.
You could use Exist subquery wrapper:
from django.db.models import Exists, OuterRef
subquery = User_Piece.objects.filter(user=user, piece=OuterRef('pk'))
Piece.objects.annotate(owned=Exists(subquery))
https://docs.djangoproject.com/en/dev/ref/models/expressions/#exists-subqueries
In newer versions of Django, you can do:
from django.db.models import Exists, OuterRef
pieces = Piece.objects.annotate(
owned=Exists(UserPiece.objects.filter(piece=OuterRef('id'), user=request.user))
)
for piece in pieces:
print(piece.owned) # prints True or False
Of course, you can replace the name owned with any name you want.
Easy approach, be careful with performance:
pk_pices = ( User_Piece
.objects
.filter(user=user)
.distinct()
.values_list( 'id', flat=True)
)
pieces = pieces.objects.filter( id__in = pk_pieces )
Also, notice that you have a n:m relation ship, you can rewrite models as:
class Piece(models.Model):
cost = models.IntegerField(default=50)
piece = models.CharField(max_length=256)
users = models.ManyToManyField(User, through='User_Piece', #<- HERE!
related_name='Pieces') #<- HERE!
And get user pieces as:
pieces = currentLoggedUser.pieces.all()