Django how to check if objects contains string in multiple fields - django

So I have a model called Puzzle which contains a title, question, and a subject. I want to be able to search for puzzles by entering a string. My search bar also contains three checkboxes:
- title
- question
- subject
I want to somehow be able to query my database to see if the ticked fields contain the search text. For example, if title and question were ticked, I would query to see if the puzzle's title contains this string OR its questions contains the string. Is there any way to query this in Django?
I know that if I wanted to check just one of these fields, for instance the title, I could just do this:
Puzzle.objects.filter(title__contains=search_text)
But I want to be able to dynamically query the fields that are ticked.
Currently, my view contains three boolean values: title, question, and subject. The boolean is True is it is ticked, and false if it is not ticked.
How can I manipulate these three booleans along with Django queries to be able to dynamically query my database?
Thank you

You can do OR queries using Q objects:
from django.db.models import Q
Puzzle.objects.filter(
Q(title__contains=search_text) |
Q(question__contains=search_text) |
Q(subject__contains=search_text)
)
Of course you can build this Q object dynamically:
q = Q()
if title:
q |= Q(title__contains=search_text)
if question:
q |= Q(question__contains=search_text)
if subject:
q |= Q(subject__contains=search_text)
# If you want no result if none of the fields is selected
if q:
queryset = Puzzle.objects.filter(q)
else:
queryset = Puzzle.objects.none()
# Or if you want all results if none of the fields is selected
queryset = Puzzle.objects.filter(q)
If you have all selected fields in a list (ie. search_fields = ['title', 'subject'], you can even make it more generic:
from functools import reduce
from operators import or_
q = reduce(or_, [Q(**{f'{f}__contains': search_text}) for f in search_fields], Q())
Puzzle.objects.filter(q)

Related

How to create a customized filter search function in Django?

I am trying to create a filter search bar that I can customize. For example, if I type a value into a search bar, then it will query a model and retrieve a list of instances that match the value. For example, here is a view:
class StudentListView(FilterView):
template_name = "leads/student_list.html"
context_object_name = "leads"
filterset_class = StudentFilter
def get_queryset(self):
return Lead.objects.all()
and here is my filters.py:
class
StudentFilter(django_filters.FilterSet):
class Meta:
model = Lead
fields = {
'first_name': ['icontains'],
'email': ['exact'],
}
Until now, I can only create a filter search bar that can provide a list of instances that match first_name or email(which are fields in the Lead model). However, this does now allow me to do more complicated tasks. Lets say I added time to the filter fields, and I would like to not only filter the Lead model with the time value I submitted, but also other Lead instances that have a time value that is near the one I submitted. Basically, I want something like the def form_valid() used in the views where I can query, calculate, and even alter the values submitted.
Moreover, if possible, I would like to create a filter field that is not necessarily an actual field in a model. Then, I would like to use the submitted value to do some calculations as I filter for the list of instances. If you have any questions, please ask me in the comments. Thank you.
You can do just about anything by defining a method on the filterset to map the user's input onto a queryset. Here's one I did earlier. Code much cut down ...
The filter coat_info_contains is defined as a CharFilter, but it is further parsed by the method which splits it into a set of substrings separated by commas. These substrings are then used to generate Q elements (OR logic) to match a model if the substring is contained in any of three model fields coating_1, coating_2 and coating_3
This filter is not implicitly connected to any particular model field. The connection is through the method= specification of the filter to the filterset's method, which can return absolutely any queryset on the model that can be programmed.
Hope I haven't cut out anything vital.
import django_filters as FD
class MemFilter( FD.FilterSet):
class Meta:
model = MyModel
# fields = [fieldname, ... ] # default filters created for these. Not required if all declarative.
# fields = { fieldname: [lookup_expr_1, ...], ...} # for specifying possibly multiple lookup expressions
fields = {
'ft':['gte','lte','exact'], 'mt':['gte','lte','exact'],
...
}
# declarative filters. Lots and lots of
...
coat_info_contains = FD.CharFilter( field_name='coating_1',
label='Coatings contain',
method='filter_coatings_contains'
)
...
def filter_coatings_contains( self, qs, name, value):
values = value.split(',')
qlist = []
for v in values:
qlist.append(
Q(coating_1__icontains = v) |
Q(coating_2__icontains = v) |
Q(coating_3__icontains = v) )
return qs.filter( *qlist )

Checking for many-to-many relation OR a property

How can I check whether a many-to-many relationship exists or another property is fulfilled? When I try the query, it returns some rows twice!
Given a model
from django.db import models
class Plug(models.Model):
name = models.CharField(primary_key=True, max_length=99)
class Widget(models.Model):
name = models.CharField(primary_key=True, max_length=99)
shiny = models.BooleanField()
compatible = models.ManyToManyField(Plug)
I have the following items in my database:
from django.db.models import Q
schuko = Plug.objects.create(name='F')
uk = Plug.objects.create(name='G')
Widget.objects.create(name='microwave', shiny=True).compatible.set([uk])
Widget.objects.create(name='oven', shiny=False).compatible.set([uk])
Widget.objects.create(name='pc', shiny=True).compatible.set([uk, schuko])
Now I want all names of widgets that are shiny and/or compatible with Schuko:
shiny_or_schuko = sorted(
Widget.objects.filter(Q(shiny=True) | Q(compatible=schuko))
.values_list('name', flat=True))
But to my surprise, this does not return ['microwave', 'pc']. Instead, 'pc' is listed twice, i.e. shiny_or_schuko is ['microwave', 'pc', 'pc'].
Is this a Django bug? If not, how can I set up the query that I get 'pc' just once?
Is this a Django bug?
No. You simply perform a LEFT OUTER JOIN with the many-to-many table. If two or more related objects match, it will be included multiple times. This can be wanted behavior, for example if you add extra annotations to the elements that takes values from these related objects.
You can make use of .distinct() [Django-doc] to return only distinct elements:
Widget.objects.filter(
Q(shiny=True) | Q(compatible=schuko)
).values_list('name', flat=True).distinct()

Filtering models based off of the concatenation of a field on a one to many relationship in django

Say I had some models defined as such:
Class Document(models.Model):
pass
Class Paragraph(models.Model):
text = models.CharField(max_length=2000)
document = models.ForeignKey(Document, related_name="paragraphs")
And I wanted to find all of the Documents that have the word "foo" contained in any of their paragraphs text fields.
Something like:
Document.objects.annotate(text=[Concatenation of all paragraphs text]).filter(text__icontains='foo')
How would I go about this in a Django way, not writing direct SQL queries.
How about this:
queryset = Paragraph.objects.filter(text__icontains='foo')
text = ''.join(obj.text for obj in queryset)
As #ReinstateMonica posted in her answer starting with filtering the Paragraph objects seems like the easiest approach. The result of this could be used to filter the documents. If you only have only one relevant text field the approach could look something like:
results = Document.objects.filter(
pk__in=Paragraph.objects.filter(
text__icontains='foo').values_list('document', flat=True)
)
If your Paragraph model contains multiple text fields you can first concat the fields and then use the same approach, so:
from django.db.models import Concat
qs = Document.objects.filter(
pk__in=Paragraph.objects.annotate(
conc_text=Concat('text', 'text2') # all relevant text fields
).filter(
conc_text__icontains='foo'
).values_list(
'document', flat=True
)
)
The Django ORM can perform queries that span relationships:
Document.objects.filter(paragraphs__text__icontains='foo')

search database from multiple form fields

I have the following case. I have a form with 3 fields which are submitted with POST method. Then the fields are captured and a search using Q is made on the database:
query = Model.objects.filter( Q(field1=field1) & Q(field2=field2) & Q(field3=field3))
The problem is that I would like to dynamically use the fields that are filled not the empty ones. That means that the query will contain one or two or three criteria depending on the user.
I have managed to perform the search I describe with nested if but considering adding extra fields it gets bigger and bigger.
thanks
You can write generic function to generate a query based on fields passed-in.
def generate_query(**kwargs):
query = Q()
for database_field, value in kwargs.items():
query_dict = {database_field:value}
if value:
query &= Q(**query_dict)
return query
And use it:
data = {"name":"john", "age__gte":25} # your post data
query = generate_query(**data)
objects = SomeModel.objects.filter(query)
# generally: SomeModel.objects.filter(request.POST)
Consider you have query field like below:
field = {'field1': 'value1', 'field2': 'value2', 'field3': None}
You can filter to remove null field as below:
non_empty_field = dict(filter(lambda x: x[1], field.items())) # output: {'field1': 'value1', 'field2': 'value2'}
For performing and query you can write
Model.objects.filter(**non_empty_field) # equivalent to Model.objects.filter(field1='value1', field2='value2')
to perform and query you don't need Q() object
For performing or query you can write:
Model.objects.filter(eval('|'.join(['(Q({}="{}"))'.format(i,j) for i,j in non_empty_field.items()])))
according to current field example above query will be equivalent to:
Model.objects.filter(Q(field1=value1) | Q(field2=value2))

Django: multiple parameters in exclude are joined by OR insted of AND

According to documentation, exclude() joines multiple parameters by AND, but my query seems to use OR logic instead.
I have the following model:
class Book(models.Model):
# some fields
authors = models.ManyToManyField(Author)
And I want to get all books except those, where is only one author and author's id is '12':
q_and = Book.objects.annotate(authors_number=Count('authors')).exclude(authors_number=1, authors__in=['12'])
But instead, I get result which is similar to query with OR logic:
q_or = Book.objects.annotate(authors_number=Count('authors')).exclude(authors_number=1).exclude(authors__in=['12'])
If I filter books with the only author and author's id='12', I get those I need to exclude:
need_to_exclude = Book.objects.annotate(authors_number=Count('authors')).filter(authors_number=1, authors__in=['12'])
I know how to make, what I want in two queries, but how to make the same with one query, using exclude()?
And what's wrong with my query?
Try this; This is the OR operation.
from django.db.models import Q
q = Q(Q(authors_number=1)| Q(authors__in=['12']))
q_and = Book.objects.annotate(authors_number=Count('authors')).exclude(q).distinct()