Annotate over a field of intermediate model django - django

I have the following scheme:
class User(AbstractUser):
pass
class Task(models.Model):
pass
class Contest(models.Model):
tasks = models.ManyToManyField('Task',
related_name='contests',
blank=True,
through='ContestTaskRelationship')
participants = models.ManyToManyField('User',
related_name='contests_participated',
blank=True,
through='ContestParticipantRelationship')
class ContestTaskRelationship(models.Model):
contest = models.ForeignKey('Contest', on_delete=models.CASCADE)
task = models.ForeignKey('Task', on_delete=models.CASCADE)
cost = models.IntegerField()
class ContestParticipantRelationship(models.Model):
contest = models.ForeignKey('Contest', on_delete=models.CASCADE)
user = models.ForeignKey('User', on_delete=models.CASCADE)
task = models.ForeignKey('Task', on_delete=models.CASCADE, related_name='contests_participants_relationship')
is_solved = models.BooleanField()
Now I get the contest object and need to fetch all tasks over the tasks field, rach annotated with count of users solved it. So, I need to count the number of ContestParticipantRelationship with needed task, needed contest and is_solved set to True. How to make such a query?

Probably something like:
from django.db.models import IntegerField, Value, Sum
from django.db.models.functions import Cast, Coalesce
Task.objects.filter(
contests__contest=some_contest,
).annotate(
nsolved=Cast(Coalesce(
Sum('contests_participants_relationship__is_solved'), Value(0)
),IntegerField())
)
So here we first filter on the fact that the contest of the task is some_contest. And next we perform a Sum(..) over the is_solved column. Since there are corner-cases where this can be NULL (in case there is no user that made an attempt, etc.), then we convert it to a 0, and furthermore we cast it to an IntegerField, since otherwise some instances might be annotated with True, and False in case zero or one user solves it.

Related

Annotating values from filtered related objects -- Case, Subquery, or another method?

I have some models in Django:
# models.py, simplified here
class Category(models.Model):
"""The category an inventory item belongs to. Examples: car, truck, airplane"""
name = models.CharField(max_length=255)
class UserInterestCategory(models.Model):
"""
How interested is a user in a given category. `interest` can be set by any method, maybe a neural network or something like that
"""
user = models.ForeignKey(User, on_delete=models.CASCADE) # user is the stock Django user
category = models.ForeignKey(Category, on_delete=models.CASCADE)
interest = models.PositiveIntegerField(default=0, validators=[MinValueValidator(0)])
class Item(models.Model):
"""This is a product that we have in stock, which we are trying to get a User to buy"""
model_number = models.CharField(max_length=40, default="New inventory item")
product_category = models.ForeignKey(Category, null=True, blank=True, on_delete=models.SET_NULL, verbose_name="Category")
I have a list view showing items, and I'm trying to sort by user_interest_category for the currently logged in user.
I have tried a couple different querysets and I'm not thrilled with them:
primary_queryset = Item.objects.all()
# this one works, and it's fast, but only finds items the users ALREADY has an interest in --
primary_queryset = primary_queryset.filter(product_category__userinterestcategory__user=self.request.user).annotate(
recommended = F('product_category__userinterestcategory__interest')
)
# this one works great but the baby jesus weeps at its slowness
# probably because we are iterating through every user, item, and userinterestcategory in the db
primary_queryset = primary_queryset.annotate(
recommended = Case(
When(product_category__userinterestcategory__user=self.request.user, then=F('product_category__userinterestcategory__interest')),
default=Value(0),
output_field=IntegerField(),
)
)
# this one works, but it's still a bit slow -- 2-3 seconds per query:
interest = Subquery(UserInterestCategory.objects.filter(category=OuterRef('product_category'), user=self.request.user).values('interest'))
primary_queryset = primary_queryset.annotate(interest)
The third method is workable, but it doesn't seem like the most efficient way to do things. Isn't there a better method than this?

Having a unqiue counter per User for a different model in Django

I would like to know what the best way to have a model Receipt where the model has an ID field that is unique to each Django User, that won't have issues with asynchronous requests. There is still a separate main ID field added by default, but I would like to have a separate ID for the receipts so that I can display this ID for each User so that their receipt numbers start from 1 when they go to their receipt list.
class Receipt(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
employee = models.CharField(max_length=255)
date = models.DateTimeField()
txn_type = models.CharField(max_length=8)
total = models.DecimalField(max_digits=10, decimal_places=2)
correction = models.ForeignKey(
ReceiptCorrection, on_delete=models.PROTECT, default=None, null=True
I understand you can check the last number and then just increment by one, but won't that have issues if two computers are running the same user and they both make a receipt at the same time?
I appreciate any help with this. Thank you!
By the "unique counter per User" you mean the code series per User. So I would create a new model ReceiptCodeSeries
from django.db import transaction, models
class ReceiptCodeSeries(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
series_count = models.IntegerField(default=0)
# Optional fields
prefix = models.CharField(default="2021")
suffix_length = models.IntegerField(default=4)
#classmethod
def get_next_code(cls, user):
with transaction.atomic():
series = cls.objects.select_for_update().get(user=user)
series.series_count += 1
series.save()
return series.prefix + str(series.ticket_series_count).zfill(series.suffix_length)
This approach locks the code series for each get_next_code call so parallel requests will be fine.
Example usage:
>>> code_series = ReceiptCodeSeries.objects.get(user=some_user)
>>> print(code_series.series_count)
0
>>> ReceiptCodeSeries.get_next_code(some_user)
"2021-0001"
>>> code_series.refresh_from_db()
>>> print(code_series.series_count)
1
If you want to learn more about this approach, please read a great article from Haki Benita - How to Manage Concurrency in Django Models

Order Django queryset by related field (with code-value)

Simplifying my model a lot, I have the following:
class Player(models.Model):
name = models.CharField(max_length=50)
number = models.IntegerField()
class Statistic(models.Model):
'''
Known codes are:
- goals
- assists
- red_cards
'''
# Implicit ID
player = models.ForeignKey(
'Player', on_delete=models.CASCADE, related_name='statistics')
code = models.CharField(max_length=50)
value = models.CharField(max_length=50, null=True)
I'm using a code-value strategy to add different statistics in the future, without the need of adding new fields to the model.
Now, I want to list all the players, so my question is:
Is there any way to order the players by their goals scored, for example?
You need to use annotate with conditional Case statement.
from django.db.models import Case, When, F, Value, IntegerField
Statistic.objects.all().annotate(
goals=Case(
When(code="goals", then=F("value")),
default=Value(0),
output_field=IntegerField()
)).order_by("-goals")
Your queryset has now additional field goals which is equal to value only if code == goals, otherwise it is equal to 0. All i had to do from now was to order by this field.
Based on #Daniel Kusy's answer, I found the inspiration to do some small modifications for my specific scenario, where ordering Players by their scored goals would be like this:
from django.db.models import Case, When, F, Value, IntegerField
Players.objects.all().annotate(
goals=Case(
When(statistics__code="goals", then=F("statistics__value")),
default=Value(0),
output_field=IntegerField()
)).order_by("-goals")

Django - Dynamically creating field names in a model (fields not saving)

I am trying to implement a voting system that keeps track of votes for each type of user on my site. My plan was to create a Vote model that keeps track of Up votes and Total votes for each user type and calculates the percentage of Up votes.
Hardcoded that looks something like this:
class Eduuser(AbstractUser):
TYPE_1 = 'TY1'
TYPE_2 = 'TY2'
...
USER_TYPE_CHOICES = (
(TYPE_1, 'Type 1'),
(TYPE_2, 'Type 2'),
...
)
user_type = models.CharField(max_length=3, choices=USER_TYPE_CHOICES)
class Vote(models.Model):
a = models.IntegerField(
default=0, name=getattr(Eduuser, 'TYPE_1')+'_up')
b = models.IntegerField(
default=0, name=getattr(Eduuser, 'TYPE_2')+'_up')
...
c = models.IntegerField(
default=0, name=getattr(Eduuser, 'TYPE_1')+'_votes')
d = models.IntegerField(
default=0, name=getattr(Eduuser, 'TYPE_2')+'_votes')
...
def perc(self):
perc_array = []
for user_type in getattr(Eduuser, 'USER_TYPE_CHOICES'):
up = float(getattr(self, user_type[0]+'_up')) #Prevent int division
votes = getattr(self, user_type[0]+'_votes')
if votes==0:
perc_array.append(0)
else:
perc_array.append(round(up/votes, 3))
return perc_array
Although I don't anticipate adding more types, I would like for the code to look cleaner. My best attempt at looping over the user types was:
class Eduuser(AbstractUser):
...
class Vote(models.Model):
for user_type in getattr(Eduuser, 'USER_TYPE_CHOICES'):
models.IntegerField(
default=0, name=user_type[0]+'_up')
models.IntegerField(
default=0, name=user_type[0]+'_votes')
def perc(self):
...
However this does not save the fields (I guess because of the lack of assignment operator).
So a couple of quick questions:
1) Is there a way to save fields without explicitly assigning them a name? Or can I convert the string name into a variable (from other posts I've read, this seems like a bad idea)?
2) Am I even approaching this voting idea logically? Part of me feels like there is a far easier approach to keeping track of votes for multiple types of users.
Any help is appreciated! Thanks!
django-model-utils can make this cleaner with it's Choices helper.
You could do a Vote model in the following way (untested):
from model_utils import Choices
class User(AbstractUser):
USER_CHOICES = Choices(
('one', 'Type 1'),
('two', 'Type 2'),
)
user_type = models.CharField(max_length=10, choices=USER_CHOICES)
class Vote(models.Model):
"""
A single vote on a `User`. Can be up or down.
"""
VOTE_CHOICES = Choices(
('upvote'),
('downvote'),
)
user = models.ForeignKey(User)
vote = models.CharField(max_length=10, choices=VOTE_CHOICES)
Example usage – get the number of positive votes for all “Type 1” Users:
# retrieve all of the votes
all_votes = Vote.objects.all()
all_votes_count = len(all_votes)
# now retrieve all of the votes for users of ‘Type 1’
type_one_votes = all_votes.filter(user__user_type=User.USER_CHOICES.one)
type_one_votes_count = len(type_one_votes)
# …and now filter the positive votes for ‘Type 1’ users
type_one_positives = type_one_votes.filter(vote=Vote.VOTE_CHOICES.upvote)
type_one_positive_vote_count = len(type_one_positives)
# etc.
Django uses some metaclass behavior to create fields based on what you declare, so this is not wholly trivial. There are some undocumented calls you can use to dynamically add fields to your model class - see this post:
http://blog.jupo.org/2011/11/10/django-model-field-injection/
That said, I would recommend a simpler approach. Create a model to hold your possible user types, then use it as a foreign key in the votes table:
class UserType(models.Model):
type_name = models.CharField()
class Vote(models.Model):
user_type = models.ForeignKey(UserType)
total = models.PositiveIntegerField()
Or track the individual votes and sum as needed, either recording the user who cast the vote or just the user's type at the time the vote was cast. Depending on what you want to do if a user changes classes after voting you might need to save the user's type anyway.
If you do just track the sums, you have to think more carefully about transaction issues - I'd say track the user and enforce a uniqueness constraint.

Django: Filter in multiple models linked via ForeignKey?

I'd like to create a filter-sort mixin for following values and models:
class Course(models.Model):
title = models.CharField(max_length=70)
description = models.TextField()
max_students = models.IntegerField()
min_students = models.IntegerField()
is_live = models.BooleanField(default=False)
is_deleted = models.BooleanField(default=False)
teacher = models.ForeignKey(User)
class Session(models.Model):
course = models.ForeignKey(Course)
title = models.CharField(max_length=50)
description = models.TextField(max_length=1000, default='')
date_from = models.DateField()
date_to = models.DateField()
time_from = models.TimeField()
time_to = models.TimeField()
class CourseSignup(models.Model):
course = models.ForeignKey(Course)
student = models.ForeignKey(User)
enrollment_date = models.DateTimeField(auto_now=True)
class TeacherRating(models.Model):
course = models.ForeignKey(Course)
teacher = models.ForeignKey(User)
rated_by = models.ForeignKey(User)
rating = models.IntegerField(default=0)
comment = models.CharField(max_length=300, default='')
A Course could be 'Discrete mathematics 1'
Session are individual classes related to a Course (e.g. 1. Introduction, 2. Chapter I, 3 Final Exam etc.) combined with a date/time
CourseSignup is the "enrollment" of a student
TeacherRating keeps track of a student's rating for a teacher (after course completion)
I'd like to implement following functions
Sort (asc, desc) by Date (earliest Session.date_from), Course.Name
Filter by: Date (earliest Session.date_from and last Session.date_to), Average TeacherRating (e.g. minimum value = 3), CourseSignups (e.g. minimum 5 users signed up)
(these options are passed via a GET parameters, e.g. sort=date_ascending&f_min_date=10.10.12&...)
How would you create a function for that?
I've tried using
denormalization (just added a field to Course for the required filter/sort criterias and updated it whenever changes happened), but I'm not very satisfied with it (e.g. needs lots of update after each TeacherRating).
ForeignKey Queries (Course.objects.filter(session__date_from=xxx)), but I might run into performance issues later on..
Thanks for any tipp!
In addition to using the Q object for advanced AND/OR queries, get familiar with reverse lookups.
When Django creates reverse lookups for foreign key relationships. In your case you can get all Sessions belonging to a Course, one of two ways, each of which can be filtered.
c = Course.objects.get(id=1)
sessions = Session.objects.filter(course__id=c.id) # First way, forward lookup.
sessions = c.session_set.all() # Second way using the reverse lookup session_set added to Course object.
You'll also want to familiarize with annotate() and aggregate(), these allow you you to calculate fields and order/filter on the results. For example, Count, Sum, Avg, Min, Max, etc.
courses_with_at_least_five_students = Course.objects.annotate(
num_students=Count('coursesignup_set__all')
).order_by(
'-num_students'
).filter(
num_students__gte=5
)
course_earliest_session_within_last_240_days_with_avg_teacher_rating_below_4 = Course.objects.annotate(
min_session_date_from = Min('session_set__all')
).annotate(
avg_teacher_rating = Avg('teacherrating_set__all')
).order_by(
'min_session_date_from',
'-avg_teacher_rating'
).filter(
min_session_date_from__gte=datetime.now() - datetime.timedelta(days=240)
avg_teacher_rating__lte=4
)
The Q is used to allow you to make logical AND and logical OR in the queries.
I recommend you take a look at complex lookups: https://docs.djangoproject.com/en/1.5/topics/db/queries/#complex-lookups-with-q-objects
The following query might not work in your case (what does the teacher model look like?), but I hope it serves as an indication of how to use the complex lookup.
from django.db.models import Q
Course.objects.filter(Q(session__date__range=(start,end)) &
Q(teacher__rating__gt=3))
Unless absolutely necessary I'd indeed steer away from denormalization.
Your sort question wasn't entirely clear to me. Would you like to display Courses, filtered by date_from, and sort it by Date, Name?