Custom query filter in django admin - python

Here is my models code:
class Quote(models.Model):
"""Quote model."""
quote_text = models.TextField(unique=True)
author = models.ForeignKey(Author)
topic = models.ForeignKey(Topic)
tags = models.ManyToManyField(Tag)
language = models.ForeignKey(Language)
hit = models.IntegerField(default=0)
published = models.BooleanField(default=False)
created = models.DateTimeField(auto_now_add=True)
modified = models.DateTimeField(auto_now=True)
I want filter all Quote based on characters length, and this is my query in django admin.
class QuoteCountFilter(admin.SimpleListFilter):
"""Filter based on quote_text characters count."""
title = _('Quote Text Char Count')
parameter_name = 'quotelength'
def lookups(self, request, model_admin):
return (
('lessthan50', _('Less than 50')),
('morethan50', _('More than 50')),
)
def queryset(self, request, queryset):
if self.value() == 'lessthan50':
return queryset.extra(select={"val": "SELECT id FROM web_quote WHERE character_length(quote_text) < 50"})
However, it returns Programming error more than one row returned by a subquery used as an expression
Any ideas how to fix?
What I am trying is to find all Quotes where quote_text length is less than 50 characters

Say goodbye to extra and say hello to Length
from django.db.models.functions import Length
queryset.annotate(len=Length('quote_text').filter(len__lt=50)
much neater, safer and shorter

Related

Django Filter name "todo__test" is not defined

I'm trying to filter my Todos by the test_id pulled from the URL. It pulls the id from the URL but it cant seem to filter with todo__test. I have also tried "test", "Todo.test.test_id", "Todo.test". I guess I'm confused about what variable I need to filter and the Django restframework documentation doesn't explicitly show what variable to use. Their example uses "purchaser__username" which I don't understand where it comes from. https://www.django-rest-framework.org/api-guide/filtering/
class TodoList(generics.ListAPIView):
queryset = Todo.objects.all()
serializer_class = TodoSerializer
def get_queryset(self):
test_id = self.kwargs['test_id']
return Todo.objects.filter(todo__test == test_id)
class Todo(models.Model):
test = models.ForeignKey(Test, on_delete=models.CASCADE)
content = models.TextField(blank=True)
order = models.IntegerField()
def __str__(self):
return self.content + ' - ' + self.test.test_name
class Meta:
ordering = ['test_id']
i guess it will be like this. you passed incorrect foreign key field name.
Todo.objects.filter(test_id='whatever_value')

Django admin list filter using a model property

I have a model such as below:
class Order(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
number = models.CharField(max_length=36, blank=True, null=True)
external_number = models.CharField(default=None, blank=True, null=True, max_length=250)
#property
def is_external(self) -> bool:
return self.external_number is not None
And I register my model like below:
#admin.register(Order)
class OrderAdmin(ReadOnlyIDAdminClass):
list_filter = ["number", "is_external"]
list_display = ["id", "is_external"]
But since is_external is not a db field, I get the following error:
(admin.E116) The value of 'list_filter[1]' refers to 'is_external', which does not refer to a Field
I have tried something like creating a custom filter:
class IsExternal(admin.FieldListFilter):
# Human-readable title which will be displayed in the
# right admin sidebar just above the filter options.
title = 'is external'
# Parameter for the filter that will be used in the URL query.
parameter_name = 'is_external'
def lookups(self, request, model_admin):
return (
('True', True),
('False', False)
)
def queryset(self, request, queryset):
if self.value():
return queryset.filter(external_number__isnull=False)
return queryset.filter(external_number__isnull=True)
and then update my Admin:
#admin.register(Order)
class OrderAdmin(ReadOnlyIDAdminClass):
list_filter = ["number", ("is_external", IsExternal)]
list_display = ["id", "is_external"]
But it raises:
Order has no field named 'is_external' which I think makes sense, but is there anyway to do this? I feel like I am messing on something.
I think after hours of trying to fix this, only now I found a working solution.
We make use of the custom filter above, but we use it directly such as:
#admin.register(Order)
class OrderAdmin(ReadOnlyIDAdminClass):
list_filter = ["number", IsExternal]
list_display = ["id", "is_external"]
This should work now; also please notice that in self.value() of our filter queryset, it is returning a string, so we should cast/ parse our values accordingly, in my filter above, I will be getting the values True or False as string.
Edit:
I have updated my custom filter to function like Django's, where we make use of values 1 and 0 for True and False.
class IsExternal(SimpleListFilter):
# Human-readable title which will be displayed in the
# right admin sidebar just above the filter options.
title = 'is external'
# Parameter for the filter that will be used in the URL query.
parameter_name = 'is_external'
def lookups(self, request, model_admin):
return (
(1, 'yes'),
(0, 'no')
)
def queryset(self, request, queryset):
if self.value() == "1":
return queryset.filter(external_number__isnull=False)
elif self.value() == "0":
return queryset.filter(external_number__isnull=True)
return queryset
Please notice that this is now using same logic defined in the model property (but not the property itself) but on the database level. If there is any way to make use directly of properties, I would love if you could share!

how to filtered 2 Q conditions in query django?

so,i have a List of all mobile phones whose brand name is one of the incoming brands. The desired brand names will be entered. The number of entries is unknown and may be empty. If the input is empty, the list of all mobile phones must be returned.
model:
from django.db import models
class Brand(models.Model):
name = models.CharField(max_length=32)
nationality = models.CharField(max_length=32)
def __str__(self):
return self.name
class Mobile(models.Model):
brand = models.ForeignKey(Brand, on_delete=models.CASCADE)
model = models.CharField(max_length=32, default='9T Pro', unique=True)
price = models.PositiveIntegerField(default=2097152)
color = models.CharField(max_length=16, default='Black')
display_size = models.SmallIntegerField(default=4)
is_available = models.BooleanField(default=True)
made_in = models.CharField(max_length=20, default='China')
def __str__(self):
return '{} {}'.format(self.brand.name, self.model)
query:
from django.db.models import F, Q
def some_brand_mobiles(*brand_names):
query = Mobile.objects.filter(Q(brand__name__in=brand_names) | ~Q(brand__name=[]))
return query
If the input is empty, the list of all mobile phones will be returned, but i cant use *brand_names to return the list.
for example
query = Mobile.objects.filter(Q(brand_name_in=['Apple', 'Xiaomi']))
return query
and
query = Mobile.objects.filter(~Q(brand__name=[]))
return query
Both of these conditions work by example alone, but it does not check both conditions with the function I wrote.
how to fix it ?
It is simpler to just check the list of brand_names and filter if it contains at least one element:
def some_brand_mobiles(*brand_names):
if brand_names:
return Mobile.objects.filter(brand__name__in=brand_names)
else:
return Mobile.objects.all()
Try this solution:
def some_brand_mobiles(*brand_names):
queryset = Mobile.objects.all()
if brand_names:
queryset = queryset.filter(brand__name__in=brand_names)
return queryset
In that way you can add more filters based on any other condition over the queryset.

How to order queryset based on best match in django-rest-framework?

I am trying to order results of a query with parameters by number of matches.
For example, let's say we have a Model:
class Template(models.Model):
headline = CharField(max_length=300)
text = TextField()
image_text = TextField(max_length=500, blank=True, null=True)
tags = TaggableManager(through=TaggedItem)
...
With a Serializer:
class TemplateSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Template
fields = (...)
And a ViewSet:
class TemplateViewSet(viewsets.ModelViewSet):
"""
API endpoint that allows Templates to be viewed or edited.
"""
queryset = Template.objects.all()
serializer_class = TemplateSerializer
def get_queryset(self):
queryset = Template.objects.all()
tags = self.request.query_params.getlist('tags', None)
search_text = self.request.query_params.getlist('search_text', None)
if tags is not None:
queries = [Q(groost_tags__name__iexact=tag) for tag in tags]
query = queries.pop()
for item in queries:
query |= item
queryset = queryset.filter(query).distinct()
if search_tags is not None:
queries = [Q(image_text__icontains=string) |
Q(text__icontains=string) |
Q(headline__icontains=string) for string in search_tags]
query = queries.pop()
for item in queries:
query |= item
queryset = queryset.filter(query).distinct()
What I need to do is count every match the filter finds and then order the queryset by that number of matches for each template. For example:
I want to find all the templates that have "hello" and "world" strings in their text, image_text or headline. So I set the query parameter "search_text" to hello,world. Template with headline="World" and text="Hello, everyone." would have 2 matches. Another one with headline="Hello" would have 1 match. The template with 2 matches would be the first in the queryset. The same behaviour should work for tags and tags with search_text combined.
I tried to calculate these numbers right in the ViewSet and then return a sorted(queryset, key=attrgetter('matches')) but encountered several issues with the DRF, like Template has no attribute 'matches'. Or 404 when directly accessing a Template instance through API.
Any ideas?
Give a try to annotation where each matching pair returns 1 or 0 that are summarized into rank:
from django.db.models import Avg, Case, F, FloatField, Value, When
Template.objects.annotate(
k1=Case(
When(image_text__icontains=string, then=Value(1.0)),
default=Value(0.0),
output_field=FloatField(),
),
k2=Case(
When(text__icontains=string, then=Value(1.0)),
default=Value(0.0),
output_field=FloatField(),
),
k3=Case(
When(headline__icontains=string, then=Value(1.0)),
default=Value(0.0),
output_field=FloatField(),
),
rank=F("k1") + F("k2") + F("k3"),
).order_by("-rank")

Search result from two different model related by foreign-key

I've two models. One is
class Pin(models.Model):
title = models.CharField(max_length=255)
def __str__(self):
return self.title
another is
class Content(models.Model):
pin = models.ForeignKey(Pin, on_delete = models.CASCADE)
content_type = models.CharField(max_length=2)
content = models.TextField()
I want show the list of Pin's if the Pin's title or Content's content contains the search query.
For a single Pin there can be multiple Content, and I want to show the list of Pins if the title of Pin or the content of a Content match.
I can generate result if the Pin's title contains the query. Anyone can help me?
General syntax of filtering is :
<ModelName>.objects.filter(
Q(<ForeignKeyField>__<field_name>__<filter_method>=<query>) |
Q(<field_name>__<filter_method>=<query>)
)
`|' here is OR operator
distinct() is used so that the identical querysets are again filtered in resulting queryset.
def get_pin_list(request, q):
pins = []
if q:
querysets = Content.objects.filter(Q(pin__title__icontains=q) |
Q(content__icontains=q)
).distinct('pin')
pins = [queryset.pin for queryset in querysets]
return render(request, 'some_page.html', {'pins': pins})
In case if it doesn't works for MySQL:
def get_pin_list(request, q):
pins = []
if q:
querysets = Content.objects.filter(Q(pin__title__icontains=q) |
Q(content__icontains=q)
).distinct()
pins = [queryset.pin for queryset in querysets]
pins = list(set(pins))
return render(request, 'some_page.html', {'pins': pins})
Try this way.
from django.db.models import Q
query = request.GET.get("q")
if query:
queryset_list = Content.objects.filter(
Q(pin__title__icontains=query)|
Q(content_type__icontains=query)|
Q(content__icontains=query)
).distinct()

Categories