Let's say I have these three models:
class Author(models.Model):
name = models.CharField(max_length=64)
class Book(models.Model):
author = models.ForeignKey(
Author,
blank=True,
null=True,
on_delete=models.SET_NULL
)
name = models.CharField(max_length=64)
class Store(models.Model):
books = models.ManyToManyField(Book)
name = models.CharField(max_length=64)
I don't know the author of some books. In these cases, the author is null.
When I query Store, I would like to know how many books each store has and sort them. So my queryset is something like this:
Store.objects.all().annotate(books_count=Count('books')).order_by('-books_count')
Now, what if I want to count only the books that have an author?
I tried this queryset, but it is clearly not correct:
filter = Q(books__author__isnull=False)
Store.objects.annotate(books_count=Count(filter)).all().order_by('-books_count')
Does anyone know the correct way to do this query?
I believe following query is what you are looking for.
books_count = (
Book.objects.filter(
store=OuterRef("pk"),
author__isnull=False,
)
.annotate(count=Func(F("id"), function="COUNT"))
.values("count")
)
Store.objects.annotate(count=Subquery(books_count)).order_by("-count")
You can go the other way around:
Book.objects.filter(author__isnull=False).values('store_set').annotate(count=Count('store_set')).order_by('-count')
I'll take a bit of a wild guess, but im thinking your usage of a Q object might be incorrect inside of theCount.
Count is expecting an expression like object, this could be a string, F, OuterRef, SubQuery and so on.
Store.objects.filter(filter).annotate(
books_count=Count("books")
).order_by('-books_count')
I think this is what you're after.
Related
I'm trying to access the grandchildren records in a list to avoid duplicate records. In this example, a tag can only be used once across articles for a given author. I will use the resulting list of grandchildren records in my clean function to return validation errors.
class Article(models.Model):
tag = models.ManyToManyField(Tag)
author = models.ForeignKey(Author, on_delete=models.CASCADE)
class Tag(models.Model):
class Author(models.Model):
Right now I can do this:
print(author.articles.first().tag.first())
Travel
I'd like to be able to use something like author.articles.tags.all() to return the list and check the submitted form against it to raise a ValidationError message to the user.
How can this be done efficiently with the basic Many-to-Many setup without creating an intermediate table for the tag relationships? This is solely in the Admin interface, in case that matters at all.
i come from the link you posted on upwork,
the way i understand your question,
what you want to achieve seems to be impossible ,
what i think can work , is to fetch articles related to the author, with their corresponding tags,
after that they are retrieved you do filtering and remove duplicates.
otherwise the tag has nothing to connect it with the author,,
I'm so jealous that's a great answer #since9teen94
Be aware, I will not base my answer in the easiest solution but how we model reality (or how we should do it).
We put tags after things exists right?
In that order I will make your life horrible with something like this:
from django.db import models
class Author(models.Model):
name = models.CharField(max_length=30, null=False)
class Article(models.Model):
name = models.CharField(max_length=30, null=False)
author = models.ForeignKey(Author, on_delete=models.CASCADE)
class Tag(models.Model):
name = models.CharField(max_length=30, null=False, unique=True)
articles = models.ManyToManyField(Article)
but believe me you don't want to follow this approach it will make your work harder.
You can search for Articles based on Tags directly
Tag.objects.filter(name='Scify').first().articles.all() # name is unique
The real issue with this is that the reverse lookup is really complex (I mean.. get ready for debug)
Article.objects.filter(
id__in=list(
Tag.objects.filter(name='Scify').first().articles.values_list('id', flat=True)
)
)
I am sure this does not solve your problem and I don't like complex code for no reason but if you're open to suggestions I don't mind to think different and add some options
Edit:
About the author and clean repeated tags.. well you don't have to deal with that and if you want to find all Tag your author has you could loop the tags
for tag in Tag.objects.all():
if tag.articles.filter(author__name='StackoverflowContributor'):
print(tag.name)
# > Scify
I am just saying that there are options not that this is the best for you but don't be afraid of the terminal, it's really cool
The Django ORM is pretty cool when you get used to it, but regular SQL is pretty cool too
# created_at, updated_at, name and tag_val are variables I
# added due to my slight ocd lol
class Author(models.Model):
name = models.CharField(max_length=255)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Tag(models.Model):
tag_val = models.CharField(max_length=255)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Article(models.Model):
author = models.ForeignKey(Author,related_name='articles', on_delete=models.CASCADE)
tags = models.ManyToManyField(Tag, related_name='articles')
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
I can write my query like this, assuming the variable 'author' has been assigned an Author object instance, and get a list of dictionaries [{'tags':1},{'tags':2}] where the value is the auto generated primary key id of the Tag object instance
author.articles.values('tags').distinct()
I'm struggling with annotations and haven't found examples that help me understand. Here are relevant parts of my models:
class Team(models.Model):
team_name = models.CharField(max_length=50)
class Match(models.Model):
match_time = models.DateTimeField()
team1 = models.ForeignKey(
Team, on_delete=models.CASCADE, related_name='match_team1')
team2 = models.ForeignKey(
Team, on_delete=models.CASCADE, related_name='match_team2')
team1_points = models.IntegerField(null=True)
team2_points = models.IntegerField(null=True)
What I'd like to end up with is an annotation on the Teams objects that would give me each team's total points. Sometimes, a team is match.team1 (so their points are in match.team1_points) and sometimes they are match.team2, with their points stored in match.team2_points.
This is as close as I've gotten, in maybe a hundred or so tries:
teams = Team.objects.annotate(total_points =
Value(
(Match.objects.filter(team1=21).aggregate(total=Sum(F('team1_points'))))['total'] or 0 +
(Match.objects.filter(team2=21).aggregate(total=Sum(F('team2_points'))))['total'] or 0,
output_field=IntegerField())
)
This works great, but (of course) annotates the total_points for the team with pk=21 to every team in the queryset. If there's a better approach for all this, I'd love to see it, but short of that, if you can show me how to turn those '21' values into a reference to the outer team's pk, I think that will work?
EDIT: I ended up using a combination of elyas' answers and annotating a raw SQL statement to solve my issues. I was not able to keep normal annotations from dropping non-unique scores from the queryset, but raw SQL seems to work.
Here's that raw annotation:
teams = Team.objects.raw('select id, sum(points) as total_points from (select team1_id as id, team1_points as points from leagueman_match union all select team2_id as id, team2_points as points from leagueman_match) group by id order by total_points desc;')
An alternative solution to annotating the QuerySet might be to make total_points a #property of the Team model (depending on the use case):
from django.db.models import Case, Q, Sum, When
class Team(models.Model):
team_name = models.CharField(max_length=50)
#property
def total_points(self):
return Match.objects.filter(Q(team1=self.id) | Q(team2=self.id)).aggregate(
total_points=Sum(Case(
When(team1=self.id, then='team1_points'),
When(team2=self.id, then='team2_points')
))
)['total_points']
The disadvantage is that it can't be used in subsequent QuerySet operations e.g. .values(), .order_by().
Django also has a #cached_property decorator which will cache the output of the attribute when it is first called.
Other solutions tried
Originally I thought you could leverage the reverse relations match_team1 and match_team2 from the Team model to generate a simple annotation:
teams = Team.objects.annotate(
total_points=(
Sum('match_team1__team1_points', distinct=True)
+ Sum('match_team2__team2_points', distinct=True)
)
)
Unfortunately this solution encounters difficulties in handling duplicates. The distinct=True argument eliminates the issue of points from the same match being summed more than once. But it introduces a different issue where different matches with the same points scored will be excluded.
Maybe I would advice to refactor your data model. Even if you find a solution for this specific problem, you may want to think little ahead.
This is a solution:
class Team(models.Model):
team_name = models.CharField(max_length=50)
class Match(models.Model):
match_time = models.DateTimeField()
team1 = models.ForeignKey(
Team, on_delete=models.CASCADE, related_name='match_team1')
team2 = models.ForeignKey(
Team, on_delete=models.CASCADE, related_name='match_team2')
class Points(models.Model):
match = models.ForeignKey(
Match, on_delete=models.CASCADE, related_name='match')
team = models.ForeignKey(
Team, on_delete=models.CASCADE, related_name='team')
points = models.IntegerField(null=True)
With this you can sum up easily the points of any team, and also filter it by matches.
class Author(models.Model):
name = models.CharField(max_length=50)
class Book(models.Model):
name = models.CharField(max_length=50)
author = models.ForeignKey(Author,related_name='book' on_delete=models.CASCADE)
is_deleted = models.BooleanField(default=False)
I want to display the number of books in each author (include zero counts).
My code:
Book.objects.filter(is_deleted =False).values('author').annotate(num=Count('id'))
but it only returns values greater than zero and lost many authors.
How can I get result like this:
author1:0,
author2:1,
author3:2,
Please help me and thanks in advance ^^.
Maybe you can try like this(using related objects):
authors = Author.objects.annotate(
book_count=Count(
'book',
filter=Q(book__is_deleted=False)
)
)
authors.values('name', 'book_count')
I'm trying to use ModelChoiceFilter to filter a database of letters based on the author. Author is a ForeignKey, and I can't seem to get it to display the "name" value of the ForeignKey.
Here is what I have:
models.py (limited to relevant bits)
class Person(models.Model):
name = models.CharField(max_length=250, verbose_name='Full Name')
...
def __str__(self):
return self.name
class Letter(models.Model):
author = models.ForeignKey(Person, related_name='author', on_delete=models.PROTECT, verbose_name='Letter Author')
recipient = models.ForeignKey(Person, related_name='recipient', on_delete=models.PROTECT, verbose_name='Recipient')
...
title = models.CharField(max_length=250, verbose_name='Title of Letter')
def __str__(self):
return self.title
letter_filters.py
class LetterFilter(django_filters.FilterSet):
...
author = django_filters.ModelChoiceFilter(queryset=Letter.objects.order_by('author__name'))
class Meta:
model = Letter
fields = ['author', 'recipient']
I can see that this kind of works. It is indeed limiting and ordering it properly, but instead of the author name being presented in the select box, it's presenting "title" from the letter (but I can tell from the title, in the proper order).
What I thought should work is this:
fields = ['author__name', 'recipient']
But that too continues to list "title" from Letter instead of "name" from Person.
I know it has what I need, because if I do:
author = django_filters.ModelChoiceFilter(queryset=Letter.objects.order_by('author__name').values('author__name'))
I get exactly what I want! But, it's presented as {'author__name':'Jane Doe'} with fields author or author_name. I just can't seem to get the right syntax.
Finally, I know I can do:
author = django_filters.ModelChoiceFilter(queryset=Person.objects.order_by('name'))
Which returns all Persons, properly ordered. However there are many more persons in the database than just authors. This is the same result as just allowing the default fields['author'... without setting the author= in the class (though unordered).
Well the queryset you specify deals with Letters, so as a result the Letters are in that cases added in the ModelChoiceFiler, which is not ideal at all.
You can however generate a list of Persons that has written at least one letter like:
django_filters.ModelChoiceFilter(
queryset=Person.objects.filter(letter_set__isnull=False).order_by('name').distinct()
)
So here we filter on the fact that the letter_set is not empty, and since this will result in a JOIN where a Person can occur multiple times, we add .distinct() to it.
I find this modeling however very weird (in your three examples). It basically means that you only can assign Persons that already wrote a Letter. What if a person that has never written a Letter wants to write a Letter?
Usually in case there are different such roles, you can for example add a BooleanField:
class Person(models.Model):
name = models.CharField(max_length=250, verbose_name='Full Name')
is_author = models.BooleanField(verbose_name='Is the person an author')
# ...
Then we can filter on Persons that are Authors:
django_filters.ModelChoiceFilter(
queryset=Person.objects.filter(is_author=True).order_by('name')
)
I am currently having the following
class Profile(models.Model):
gender = models.BooleanField()
class Q(models.Model):
name = models.CharField(max_length=128)
class A(models.Model):
q = models.ForeignKey(Q)
profile = models.ForeignKey(Profile)
What I try to do is to query Q and get the number of Answers given by either male or female?
Unfortunately, I don't even have a better idea than writing custom SQL, which I'd rather avoid for the sake of database portability. How would I start?
You're there. You just need to actually use the right filter syntax. If you're trying filter on the gender field on profile, then you do that with profile__gender, so:
males_answering = question.answers.filter(profile__gender="male").count()