Django How To Query ManyToMany Relationship Where All Objects Match - django

I have the following models:
## Tags for issues
class issueTags(models.Model):
name = models.CharField(max_length=400)
class issues(models.Model):
tags = models.ManyToManyField(issueTags,blank = True)
In my view I get an array from some client side JavaScript i.e.
(Pdb) array_data = request.POST['arr']
(Pdb) array_data
'["2","3"]'
How should I filter my issues object to find all issues which match all tags in the array? (the 2,3 are the ID values for tag__id.
If there is a better way to arrange the objects that would also work so I can search in this fashion.

At the time of writing this, the existing answers are either incorrect (e.g. filtering matching all Issues that have any of the specified tags and the correct tag count) or inefficient (e.g. attaching filters in a loop).
For the following models:
class IssueTag(models.Model):
name = models.CharField(max_length=400, blank=True)
class Issue(models.Model):
label = models.CharField(max_length=50, blank=True)
tags = models.ManyToManyField(IssueTag, related_name='issues')
I suggest using Django Annotation in conjunction with a filter like so:
from django.db.models import Count, Q
tags_to_match = ['tag1', 'tag2']
issues_containing_all_tags = Issue.objects \
.annotate(num_correct_tags=Count('tags',
filter=Q(tags__name__in=tags_to_match))) \
.filter(num_correct_tags=2)
to get all Issues that have all required tags (but may have additional tags, as is required in the question).
This will produce the following SQL query, that resolves all tag matching in a single IN clause:
SELECT "my_app_issue"."id", "my_app_issue"."label",
COUNT("my_app_issue_tags"."issuetag_id")
FILTER (WHERE "my_app_issuetag"."name" IN ('tag1', 'tag2'))
AS "num_correct_tags"
FROM "my_app_issue"
LEFT OUTER JOIN "my_app_issue_tags" ON ("my_app_issue"."id" = "my_app_issue_tags"."issue_id")
LEFT OUTER JOIN "my_app_issuetag" ON ("my_app_issue_tags"."issuetag_id" = "my_app_issuetag"."id")
GROUP BY "my_app_issue"."id", "my_app_issue"."label"
HAVING COUNT("my_app_issue_tags"."issuetag_id")
FILTER (WHERE ("my_app_issuetag"."name" IN ('tag1', 'tag2'))) = 2;
args=('tag1', 'tag2', 'tag1', 'tag2', 2)

I haven't tested this, but I think you could do the following:
from django.db.models import Q
array_data = array_data.split(',')
issues.objects.filter(
tags__in=array_data,
).exclude(
# Exclude any that aren't in array_data
~Q(tags__in=array_data)
).annotate(
matches=Count(tags, distinct=True)
).filter(
# Make sure the number found is right.
matches=len(array_data)
)
FYI, you should be using Issue, IssueTag for your model names to follow Django's naming pattern.

It isn't most elegant solution or pythonic but I ended up just looping around the resulting filter.
def filter_on_category(issue_object,array_of_tags):
#keep filtering to make an and
i = 0
current_filter = issue_object
while (i < (len(array_of_tags))):
#lets filter again
current_filter=current_filter.filter(tags__id__in=array_of_tags[i])
i=i+1
return current_filter

Django field lookups argument (__) for many-to-many fields needs list argument. I have created a dummy list for each array element of IssueTags and pass it to lookups argument and it works as expected.
Let you have this models:
class IssueTags(models.Model):
name = models.CharField(max_length=400)
class Issues(models.Model):
tags = models.ManyToManyField(IssueTags,blank = True)
You want to get Issues which contains all of these IssueTags = ["1","2","3"]
issue_tags_array = ["1","2","3"]
#First initialize queryset
queryset = Issues.objects.all()
i = 0
while i < len(issue_tags_array):
#dummy issue_tag list
issue_tag = [issue_tags_array[i]]
#lets filter again
queryset = queryset.filter(tags__id__in=issue_tag)
i=i+1
return queryset

Related

Django Q object not chaining correctly

I'm querying a ManyToMany field (tags). The values come from a list:
tag_q = Q()
tag_list = ["work", "friends"]
for tag in tag_list:
tag_q &= Q(tags__tag=tag)
Post.objects.filter(tag_q)
When I have only one value, it works flawlessly, but when more objects are stacked it always return an empty queryset.
I didn't used tags__tag__in=tag_list because it returned any post that contained any of the tags in the tag list (an OR filter), and I need an AND filter here.
This is my models:
class Tag(models.Model):
tag = models.CharField(max_length=19, choices=TagChoices.choices())
class Post(models.Model):
tags = models.ManyToManyField(Tag, related_name='posts', blank=True)
This is the Q object that is being passed in the filter query:
(AND: ('tags__tag', 'work'), ('tags__tag', 'friends')
You can not work with a Q object like that: a filter is an existential quantifier, not a universal. It means you are looking for a single Tag that has as tag name work and friends at the same time.
What you can do is work with a list of tags, and then count if the number of Tags is the same as the number of items to search for, like:
tag_list = ['work', 'friends']
Post.objects.filter(tags__tag__in=tag_list).annotate(
ntags=Count('tags')
).filter(ntags=len(set(tag_list))
since django-3.2, you can work with .alias(…) [Django-doc] instead of .annotate(…) [Django-doc]:
tag_list = ['work', 'friends']
Post.objects.filter(tags__tag__in=tag_list).alias(
ntags=Count('tags')
).filter(ntags=len(set(tag_list))

How to sort a result of objects shown after a search with keyword in Django

Actually i have a working code but the issue that i am facing is how to sort the result of the queryset based on multiple rule.
This is my models.py :
class Node(MPTTModel):
parent = TreeForeignKey('self', on_delete=models.CASCADE, blank=True, null=True, related_name='children')
name = models.TextField(blank=True, null=True)`
viewed_by = models.ManyToManyField(CustomUser, related_name='viewed_by', blank=True)
bookmarked_by = models.ManyToManyField(CustomUser, related_name='bookmarked_by', blank=True)
thumbs_up = models.ManyToManyField(CustomUser, related_name='thumbs_up', blank=True)
In my views.py i have managed to queryset the database and show all the results based on all matching words, but the missing point here is that i have managed only to sort the result by the number of bookmarks.
For i.e :
i have this two objects :
Object 1 : name = How to use django forms ?
Object 2 : name = Using django forms for searching the database.
With object 1 is bookmarked by 20 users and Object 2 is bookmarked by 10 users and i type in my search bar : Using django forms database
In the result i have the first object as the first answer shown in the list even if the second one have much more matchs with the searched keywords.
So what i want to do here is to sort the result first based on the number of matching keywords and than sort it by number of bookmarks.
This my view so far :
search_text_imported = request.session['import_search_text']
if search_text_imported != '':
result_list = []
get_result_list = [x for x in search_text_imported.split() if len(x) > 2]
for keyword in get_result_list:
tree_list = Node.objects.filter((Q(name__icontains=keyword) | Q(Tags__icontains=keyword)), tree_type='root', published=True ).annotate(num_bookmarks=Count('bookmarked_by')).order_by('-num_bookmarks')
result_list += list(tree_list)
result = list(OrderedDict.fromkeys(result_list))
context = {
'tree_result': result,
}
Please let me know if there is something missing here, any help will be much appreciated.
The issue you are having is due to the fact you are creating the result list by concatenating query results together, it does not matter that the queries are sorted if you are concatenating them. You can change your code to only perform a single sorted query by first creating your Q filter and then passing it to a single query
filter = Q()
for keyword in keywords:
filter |= Q(name__icontains=keyword)
filter |= Q(Tags__icontains=keyword
result = Node.objects.filter(
filter,
tree_type='root',
published=True
).annotate(
num_bookmarks=Count('bookmarked_by')
).order_by('-num_bookmarks')
To order by the number of keywords that were matched is a difficult problem. A potential solution is to annotate each Node with a 1 or a 0 for each keyword depending on if there was a match or not and then sum them all
from functools import reduce
from operator import add
from django.db.models import Case, When, Value, F
cases = {}
for i, keyword in enumerate(keywords):
cases[f'keyword_match_{i}'] = Case(
When(name__icontains=keyword, then=1),
default=Value(0),
output_field=models.IntegerField(),
)
Node.objects.annotate(**cases).annotate(
matches=reduce(add, (F(name) for name in cases))
).order_by('-matches')
All together
filter = Q()
cases = {}
for i, keyword in enumerate(keywords):
filter |= Q(name__icontains=keyword)
filter |= Q(Tags__icontains=keyword
# Case is basically an "if" statement
# If the condition matches then we set the annotated value to 1
cases[f'keyword_match_{i}'] = Case(
When(name__icontains=keyword, then=1),
default=Value(0),
output_field=models.IntegerField(),
)
result = Node.objects.filter(
filter,
tree_type='root',
published=True
).annotate(
**cases
).annotate(
num_bookmarks=Count('bookmarked_by'),
keywords_matched=reduce(add, (F(name) for name in cases))
).order_by('-keywords_matched', '-num_bookmarks')

Django queryset get max id's for a filter

I want to get a list of max ids for a filter I have in Django
class Foo(models.Model):
name = models.CharField()
poo = models.CharField()
Foo.objects.filter(name__in=['foo','koo','too']).latest_by_id()
End result a queryset having only the latest objects by id for each name. How can I do that in Django?
Edit: I want multiple objects in the end result. Not just one object.
Edit1: Added __in. Once again I need only latest( as a result distinct) objects for each name.
Something like this.
my_id_list = [Foo.objects.filter(name=name).latest('id').id for name in ['foo','koo','too']]
Foo.objects.filter(id__in=my_id_list)
The above works. But I want a more concise way of doing it. Is it possible to do this in a single query/filter annotate combination?
you can try:
qs = Foo.objects.filter(name__in=['foo','koo','too'])
# Get list of max == last pk for your filter objects
max_pks = qs.annotate(mpk=Max('pk')).order_by().values_list('mpk', flat=True)
# after it filter your queryset by last pk
result = qs.filter(pk__in=max_pks)
If you are using PostgreSQL you can do the following
Foo.objects.order_by('name', '-id').distinct('name')
MySQL is more complicated since is lacks a DISTINCT ON clause. Here is the raw query that is very hard to force Django to generate from ORM function calls:
Foo.objects.raw("""
SELECT
*
FROM
`foo`
GROUP BY `foo`.`name`
ORDER BY `foo`.`name` ASC , `foo`.`id` DESC
""")

Query intermediate through fields in django

I have a simple Relation model, where a user can follow a tag just like stackoverflow.
class Relation(models.Model):
user = AutoOneToOneField(User)
follows_tag = models.ManyToManyField(Tag, blank=True, null=True, through='TagRelation')
class TagRelation(models.Model):
user = models.ForeignKey(Relation, on_delete=models.CASCADE)
following_tag = models.ForeignKey(Tag, on_delete=models.CASCADE)
pub_date = models.DateTimeField(default=timezone.now)
class Meta:
unique_together = ['user', 'following_tag']
Now, to get the results of all the tags a user is following:
kakar = CustomUser.objects.get(email="kakar#gmail.com")
tags_following = kakar.relation.follows_tag.all()
This is fine.
But, to access intermediate fields I have to go through a big list of other queries. Suppose I want to display when the user started following a tag, I will have to do something like this:
kakar = CustomUser.objects.get(email="kakar#gmail.com")
kakar_relation = Relation.objects.get(user=kakar)
t1 = kakar.relation.follows_tag.all()[0]
kakar_t1_relation = TagRelation.objects.get(user=kakar_relation, following_tag=t1)
kakar_t1_relation.pub_date
As you can see, just to get the date I have to go through so much query. Is this the only way to get intermediate values, or this can be optimized? Also, I am not sure if this model design is the way to go, so if you have any recomendation or advice I would be very grateful. Thank you.
You need to use Double underscore i.e. ( __ ) for ForeignKey lookup,
Like this :
user_tags = TagRelation.objects.filter(user__user__email="kakar#gmail.com").values("following_tag__name", "pub_date")
If you need the name of the tag, you can use following_tag__name in the query and if you need id you can use following_tag__id.
And for that you need to iterate through the result of above query set, like this:
for items in user_tags:
print items['following_tag__name']
print items['pub_date']
One more thing,The key word values will return a list of dictionaries and you can iterate it through above method and if you are using values_list in the place of values, it will return a list of tuples. Read further from here .

How can django order a queryset such that specific instances are listed first?

I need to order my queryset in such a way:
>>tag_queryset
[tag4, tag3, tag1, tag2]
Default queryset is:
>>Tag.objects.all()
[tag1, tag2, tag3, tag4]
So I have a specific queryset like:
>>tags_to_place_first
[tag4, tag3]
And I want to merge it with the common queryset of all() objects in Tag model to place tag4 and tag3 first in the resulting sequence:
#something like this
>>Tag.objects.all().placeFirst(tags_to_place_first)
[tag4, tag3, tag1, tag2]
Can anyone give a good solution for this that does not hinder performance?
You are talking about a performance solution, then, the approach is that. For your tag model append a new property named 'sort_field':
from django.db import models
class Tag(models.Model):
name = models.CharField(max_length=30)
sort_field = models.IntegerField()
Populate model as your convenience:
Tab.objects.filter( name = 'tag4' ).update( sort_field = 1 )
Tab.objects.filter( name = 'tag3' ).update( sort_field = 2 )
Finally sort by new sort_field:
Tag.objects.all().order_by( 'sort_field' )
You can merge querysets like:
merged_tags_qs = tags_to_place_first | other_tags
valid for lists or single instances:
merged_tags_qs = [tag4, tag3] | other_tags
merged_tags_qs = tag4 | other_tags
It's one of the django's magic features :D
If you want to remove the tags in tags_to_place_first from the queryset:
other_tags = Tag.objects.exclude(pk__in=[tag.pk for tag in tags_to_place_first])
merged_tags_qs = tags_to_place_first | other_tags