We are trying to return a list of titles for the Django API, in which the title can have a few keywords.
So for instance, if we use the __icontains method to search for "money" and "world" (api.com/?keyworld=money&keyword=world) this will return all records that contain money, world or both.
The related SQL statement is:
select * from news
where news_source = 1 or news_source = 2
and news_title like '%money%' or news_title like '%world%'
We are trying to use this code to allow the user to have multiple keywords for the __icontains as well as multiple sources, so the end goal is:
api.com/?keyworld=money&keyword=world&source=1&source=2
Our code:
def get_queryset(self):
queryset = News.objects.all()
title = self.request.query_params.getlist('title')
source = self.request.query_params.getlist('source')
if title:
queryset = queryset.filter(news_title__icontains=title, news_source__in=source)
return queryset
The issue is that this is only returning the second keyword if a second keyword is used, and not other keywords prior to what is typed in &keyword=.
You can not perform an __icontains with a list, but you can for example design a function that, for a list constructs the logical or of these values. For example:
from django.db.models import Q
from functools import reduce
from operator import or_
def or_fold(list_of_qs):
if list_of_qs:
return reduce(or_, list_of_qs)
else:
return Q()
def unroll_lists_or(qs, **kwargs):
return qs.filter([
or_fold(Q(**{k: vi}) for vi in v)
for k, v in kwargs.items()
])
You can then call the unroll_lists_or with a queryset, and each item should be an iterable (for example a list). It will then perform or-logic between the items of the list, and and-logic between different keys. In case an iterable is empty, it is ignored.
So we can then write the check as:
unroll_lists_or(queryset, news_title__icontains=title, news_source=source)
In case title contains two items (so title == [title1, title2]), and source contains three items (so source = [source1, source2, source3]), then this will result in:
qs.filter(
Q(news_title__icontains=title1) | Q(news_title__icontains=title2),
Q(news_source=source1) | Q(news_source=source2) | Q(news_source=source3)
)
You can however combine it with an .filter(..) for the __in check. For example:
queryset = News.objects.all()
if source:
queryset = queryset.filter(news_source__in=source)
queryset = unroll_lists_or(queryset, news_title__icontains=title)
I was able to solve this by creating 2 separate functions within the get_querset() function, which is called when a GET request is made.
def get_queryset(self):
queryset = News.objects.all()
source_list = self.request.query_params.getlist('source')
keyword_list = self.request.query_params.getlist('title')
if source_list or keyword_list:
def create_q_source(*args):
list = [*args]
source = Q()
for value in list:
source.add(Q(news_source=value), Q.OR)
return source
def create_q_keyword(*args):
list = [*args]
keyword = Q()
for value in list:
keyword.add(Q(news_title__icontains=value), Q.OR)
return keyword
queryset = queryset.filter(create_q_source(*source_list),create_q_keyword(*keyword_list))
return queryset
Edit:
When you go to the api link and pass in the parameters, filtering will occur based on what is passed in:
http://127.0.0.1:8000/api/notes/?keyword=trump&keyword=beyond&keyword=money&source=1
SQL Equivalent:
select * from news where news_source = 1 AND news_title like '%beyond%' OR news_title like '%money%'
Related
I have the following view for filter Order objects based on a list of ids:
class GetOrdersByIDs(generics.ListAPIView):
serializer_class = OrderSerializer
def get_queryset(self):
print(self)
ids = self.kwargs.get('ids')
return Order.objects.filter(id__in=ids)
I want to receive a list of ids from URL like this: myurl/ids/1,2,3,4, but I think I have to use a regular expression like this \d+(,\d+)*$, but I don't know how to get a list from this.
I think you should use query_param for this. sth like: .../?id=1&id=2&id=3.
Then when you use request.GET.getlist('id'), it will return you a list like [1,2,3]:
...
def get_queryset(self):
ids = self.request.GET.getlist('id')
return Order.objects.filter(id__in=ids)
I have a normal Django view that returns the API for a query set. It takes query params from the URL and filters the database based on the parameters. It also outputs a maximum length of 3 "Part" objects.
I would like to add something so that it returns information on whether the queryset is clipped by the maximum length of 3. The idea is that since the inputs the query parameters, if the parameters are too vague, then there will be too much data being queried from the database. So it is clipped but then the user needs to know that it was clipped.
The current code looks like this
class PartList(generics.ListAPIView):
serializer_class = PartSerializer
def get_queryset(self):
"""
Optionally restricts the returned purchases to a given user,
by filtering against a `username` query parameter in the URL.
"""
queryset = Part.objects.all()
querydict = self.request.query_params
for (k, value) in querydict.items():
search_type = 'contains'
filter = k + '__' + search_type
queryset = queryset.filter(**{filter: value})
query_max_limit = 3
return queryset[:min(len(queryset), query_max_limit)]
You can try to fetch four elements, and in case it returns four, you display the first three, and specify that the data is clipped, like:
def get_queryset(self):
"""
Optionally restricts the returned purchases to a given user,
by filtering against a `username` query parameter in the URL.
"""
queryset = Part.objects.all()
querydict = self.request.query_params
for (k, value) in querydict.items():
search_type = 'contains'
filter = k + '__' + search_type
queryset = queryset.filter(**{filter: value})
query_max_limit = 3
qs = queryset[:query_max_limit+1]
self.clipped = clipped = len(qs) > query_max_limit
if clipped:
return list(qs)[:query_max_limit]
else:
return qs
So here the get_queryset will return a collection (not per se a QuerySet), containing at most three elements, and it will set an attribute self.clipped that specifies if the data was clipped.
Or a more elegant approach would be to first count, and then slice:
def get_queryset(self):
"""
Optionally restricts the returned purchases to a given user,
by filtering against a `username` query parameter in the URL.
"""
queryset = Part.objects.all()
querydict = self.request.query_params
for (k, value) in querydict.items():
search_type = 'contains'
filter = k + '__' + search_type
queryset = queryset.filter(**{filter: value})
query_max_limit = 3
qs = queryset[:query_max_limit+1]
self.clipped = clipped = qs.count() > query_max_limit
if clipped:
return queryset[:query_max_limit]
else:
return qs
It might be better to move this "clipping" logic to a dedicated function, and return if it is clipped, instead of setting an attribute.
It's perfectly fine to pass metadata along with your results, like so:
{
"is_clipped": true,
"results": [
…
]
}
Willem's answer is a good way to set is_clipped.
But I think you are interested in pagination, which is a standard way to communicate to clients that the results are clipped. It's possible combine your queryset filering with pagination. By the way, I suggest you use django-filter instead of rolling your own filtering.
I'm using Django-filter to allow the user to filter a database based on multiple choices in two fields. The filterset.py looks like this:
class TapFilter(django_filters.FilterSet):
bar__region = django_filters.MultipleChoiceFilter(choices=CHOICES, label="Regions:", widget=forms.CheckboxSelectMultiple,help_text="")
bar = django_filters.ModelMultipleChoiceFilter(queryset=Bar.objects.all(), label="Bars:", widget=forms.CheckboxSelectMultiple,help_text="")
However, this functions as an AND between the two lists. I need OR instead. That is, I need to show anything matching the selection in either category.
I have seen similar questions using normal filters, but I would prefer to keep using django-filter if possible.
The website in question is here: http://bestap.pythonanywhere.com/
Update: I've put this in my filtersets.py, but am clearly not doing things right...
class TapFilter(django_filters.FilterSet):
bar__region = django_filters.MultipleChoiceFilter(choices=CHOICES, label="Regions:", widget=forms.CheckboxSelectMultiple,help_text="")
bar = django_filters.ModelMultipleChoiceFilter(queryset=Bar.objects.all(), label="Bars:", widget=forms.CheckboxSelectMultiple,help_text="")
def qs(self):
base_qs = Bar.objects.all()
qs = Bar.objects.none()
for name, filter_ in six.iteritems(self.filters):
value = self.form.cleaned_data[name]
qs = qs | filter_.filter(base_qs, value)
return qs
This gives me the error 'function' object has no attribute 'count'.
You'll need to override qs on your TapFilter FilterSet subclass.
The base implementation is not that complicated; the essence of it loops over the filters applying them to the queryset.
Simplified:
for name, filter_ in six.iteritems(self.filters):
value = self.form.cleaned_data[name]
qs = filter_.filter(qs, value)
You need the union of the filters' QuerySets, which you can get because QuerySet implements __or__, so (again simplified) you'll need something like:
base_qs = Bar.objects.all()
qs = Bar.objects.none()
for name, filter_ in six.iteritems(self.filters):
value = self.form.cleaned_data[name]
qs = qs | filter_.filter(base_qs, value)
Hopefully that gets you started.
def index(request):
expiring_list = probe.objects.filter("isExpired=True")
output = ', '.join([p.serial for p in expiring_list])
return HttpResponse(output)
isExpired is a Boolean function. How should I modify the filter so that the filter does not raise a ValueError?
You are making the query in a wrong format.
Your query should be of the form:
expiring_list = probe.objects.filter(isExpired = True)
This was the query you needed to make in case isExpired was your model field. But since you say its a function, assuming that function is inside the class you need to get all the objects in the following way:
expiring_list = []
objects = probe.objects.all()
for obj in objects:
if obj.isExpired() == True:
expiring_list.append(obj)
The expiring_list will now contain all the objects of the model probe where isExpired function returns True
I think isExpired is not a field in your models, as reference to your previous question Refresh a field from another table [Django]
I think exp_date is the field which you are looking for.
Try this:
import datetime
def index(request):
expiring_list = probe.objects.filter(exp_date__lt=datetime.date.today())
output = ', '.join([p.serial for p in expiring_list])
return HttpResponse(output)
I want to filter results in the tastypie to get results that conform to both of two filters on the same field.
So if I have a simple model like this...
class Item(models.Model):
name = models.CharField(max_length=255)
description = models.TextField()
With a ModelResource...
class ItemResource(ModelResource):
...
class Meta():
queryset = Item.objects.all()
resource_name = 'item'
filtering = {'name': ALL, 'description': ALL}
I can easily construct 'AND' queries in the url of tastypie:
/api/v1/item/?name__contains=hello&description__contains=foo
But if I want to construct an AND operator on the same field, it only takes the second argument and ignores the first. That is,
/api/v1/item/?name__contains=hello&name__contains=world
returns resources whose name field contains 'world' but not those whose name field contains BOTH 'hello' and 'world'.
I understand how to do this directly in django:
Item.objects.filter(name__contains='hello').filter(name__contains='world')
But how do I construct this kind of a query in the URL of the tastypie?
I'm using the below. It will give you support for name__contains=hello,world. And you could also do negations name__contains!=foo.
def build_filters(self, filters=None):
"""
Adds support for negation filtering
"""
if not filters:
return filters
applicable_filters = {}
self.filters = filters
# Normal filtering
filter_params = dict([(x, filters[x]) for x in filter(lambda x: not x.endswith('!'), filters)])
applicable_filters['filter'] = super(MainBaseResource, self).build_filters(filter_params)
# Exclude filtering
exclude_params = dict([(x[:-1], filters[x]) for x in filter(lambda x: x.endswith('!'), filters)])
applicable_filters['exclude'] = super(MainBaseResource, self).build_filters(exclude_params)
return applicable_filters
def apply_filters(self, request, applicable_filters):
"""
Adds support for:
1. negation filtering: value_date__year!=2013
2. multiple filtering value_date__year=2013,2012
"""
from django.db.models import Q
import operator
from types import *
objects = self.get_object_list(request)
f = applicable_filters.get('filter')
if f:
# Q Filters for multiple values (1,2,3 etc)
q_filters = []
for key, val in f.iteritems():
string = str(val)
if ',' in string:
for excl_filter in string.split(','):
q_filters.append((key, excl_filter))
q_list = [Q(x) for x in q_filters]
for x in q_filters:
try:
del f[x[0]]
except:
pass
if q_list:
objects = objects.filter(reduce(operator.or_, q_list), **f)
else:
objects = objects.filter(**f)
e = applicable_filters.get('exclude')
if e:
objects = objects.exclude(**e)
return objects