HTML input textbox in Django admin.py filter
Asked Answered
E

4

16

I would like to filter data in Django (admin.py) with text writen in HTML input textbox. I need to filter companies by city in which they are and list of all cities is too long. I would like to replace list of all cities in filter by one text input. I found something similar here http://djangosnippets.org/snippets/2429/ but there are two problems:

  1. author did not posted models.py, so it is difficuilt to change code for my needs (+ no comments)
  2. there is used class UserFieldFilterSpec(RelatedFilterSpec): but I need to use AllValuesFilterSpec instead of RelatedFilterSpec (more in file django/contrib/admin/filterspecs.py), because list of towns are in the same class as comapny (there shoud by class of towns and they should be referencing to company by foreign key (ManyToMany relationship), but for some reasons it have to be done this way)

important part of models.py looks something like this

class Company(models.Model):
    title = models.CharField(max_length=150,blank=False)
    city = models.CharField(max_length=50,blank=True)

and something from admin.py

class CatalogAdmin(admin.ModelAdmin):
    form = CatalogForm
    list_display = ('title','city') 
    list_filter = ['city',]

So again, I need to: 1. instead of list od cities display one text input in Django filter 2. After inputing city neme in that text input, filter data by city (request for filtering can be sent with some submit button or through javascript)

Thank yoy for all posts.

Evince answered 3/7, 2011 at 13:37 Comment(0)
B
22

In case anybody still need this. It is little hackish in template, but implemented without a piece of js.

filters.py:

from django.contrib.admin import ListFilter
from django.core.exceptions import ImproperlyConfigured


class SingleTextInputFilter(ListFilter):
    """
    renders filter form with text input and submit button
    """
    parameter_name = None
    template = "admin/textinput_filter.html"

    def __init__(self, request, params, model, model_admin):
        super(SingleTextInputFilter, self).__init__(
            request, params, model, model_admin)
        if self.parameter_name is None:
            raise ImproperlyConfigured(
                "The list filter '%s' does not specify "
                "a 'parameter_name'." % self.__class__.__name__)

        if self.parameter_name in params:
            value = params.pop(self.parameter_name)
            self.used_parameters[self.parameter_name] = value

    def value(self):
        """
        Returns the value (in string format) provided in the request's
        query string for this filter, if any. If the value wasn't provided then
        returns None.
        """
        return self.used_parameters.get(self.parameter_name, None)

    def has_output(self):
        return True

    def expected_parameters(self):
        """
        Returns the list of parameter names that are expected from the
        request's query string and that will be used by this filter.
        """
        return [self.parameter_name]

    def choices(self, cl):
        all_choice = {
            'selected': self.value() is None,
            'query_string': cl.get_query_string({}, [self.parameter_name]),
            'display': _('All'),
        }
        return ({
            'get_query': cl.params,
            'current_value': self.value(),
            'all_choice': all_choice,
            'parameter_name': self.parameter_name
        }, )

templates/admin/textinput_filter.html:

{% load i18n %}
<h3>{% blocktrans with filter_title=title %} By {{ filter_title }} {% endblocktrans %}</h3>

{#i for item, to be short in names#}
{% with choices.0 as i %}
<ul>
    <li>
        <form method="get">
            <input type="search" name="{{ i.parameter_name }}" value="{{ i.current_value|default_if_none:"" }}"/>

            {#create hidden inputs to preserve values from other filters and search field#}
            {% for k, v in i.get_query.items %}
                {% if not k == i.parameter_name %}
                    <input type="hidden" name="{{ k }}" value="{{ v }}">
                {% endif %}
            {% endfor %}
            <input type="submit" value="{% trans 'apply' %}">
        </form>
    </li>

    {#show "All" link to reset current filter#}
    <li{% if i.all_choice.selected %} class="selected"{% endif %}>
        <a href="{{ i.all_choice.query_string|iriencode }}">
            {{ i.all_choice.display }}
        </a>
    </li>
</ul>
{% endwith %}

Then according to your models in admin.py:

class CatalogCityFilter(SingleTextInputFilter):
    title = 'City'
    parameter_name = 'city'

    def queryset(self, request, queryset):
        if self.value():
            return queryset.filter(city__iexact=self.value())

class CatalogAdmin(admin.ModelAdmin):
    form = CatalogForm
    list_display = ('title','city') 
    list_filter = [CatalogCityFilter,]

Ready to use filter would look like this.

Bygone answered 14/12, 2013 at 22:27 Comment(6)
Thank you very much for this code snippet! You just saved me some hours of work. There is one small error in your example however: The CatalogCityFilter.queryset call should return the queryset.Acetylene
@Acetylene Does this code raise an exception in your case? As I see from django sources: for filter_spec in self.filter_specs: new_qs = filter_spec.queryset(request, qs) if new_qs is not None: qs = new_qs github.com/django/django/blob/master/django/contrib/admin/views/… if filter returns None nothing happens.Bygone
No Exception, but it did not work before returning the queryset, as the filter returns a new copy of the queryset with the filter applied.Acetylene
@Acetylene Oh,yes, I'm sorry, now I see. Just looked in my code in existing project, not my answer. Must've been some python fairies took away my return :) Fixed it.Bygone
I've been looking for this solution for 2 months. Thank you very much kind sir!Linter
I found this article with another solution that worked for me on Django 3.2.Danettedaney
S
5

I'm running Django 1.10, 1.11 and r_black's solution didn't completely fit because Django was complaining that filter fields must inherit from 'FieldListFilter'.

So a simple change for the filter to inherit from FieldListFilter took care of Django complaining and not having to specify a new class for each field, both at the same time.

class SingleTextInputFilter(admin.FieldListFilter):
    """
    renders filter form with text input and submit button
    """

    parameter_name = None
    template = "admin/textinput_filter.html"

    def __init__(self, field, request, params, model, model_admin, field_path):
        super().__init__(field, request, params, model, model_admin, field_path)
        if self.parameter_name is None:
            self.parameter_name = self.field.name

        if self.parameter_name in params:
            value = params.pop(self.parameter_name)
            self.used_parameters[self.parameter_name] = value

    def queryset(self, request, queryset):
        if self.value():
            return queryset.filter(imei__icontains=self.value())

    def value(self):
        """
        Returns the value (in string format) provided in the request's
        query string for this filter, if any. If the value wasn't provided then
        returns None.
        """
        return self.used_parameters.get(self.parameter_name, None)

    def has_output(self):
        return True

    def expected_parameters(self):
        """
        Returns the list of parameter names that are expected from the
        request's query string and that will be used by this filter.
        """
        return [self.parameter_name]

    def choices(self, cl):
        all_choice = {
            'selected': self.value() is None,
            'query_string': cl.get_query_string({}, [self.parameter_name]),
            'display': _('All'),
        }
        return ({
            'get_query': cl.params,
            'current_value': self.value(),
            'all_choice': all_choice,
            'parameter_name': self.parameter_name
        }, )

templates/admin/textinput_filter.html (unchanged):

{% load i18n %}
<h3>{% blocktrans with filter_title=title %} By {{ filter_title }} {% endblocktrans %}</h3>

{#i for item, to be short in names#}
{% with choices.0 as i %}
<ul>
    <li>
        <form method="get">
            <input type="search" name="{{ i.parameter_name }}" value="{{ i.current_value|default_if_none:"" }}"/>

            {#create hidden inputs to preserve values from other filters and search field#}
            {% for k, v in i.get_query.items %}
                {% if not k == i.parameter_name %}
                    <input type="hidden" name="{{ k }}" value="{{ v }}">
                {% endif %}
            {% endfor %}
            <input type="submit" value="{% trans 'apply' %}">
        </form>
    </li>

    {#show "All" link to reset current filter#}
    <li{% if i.all_choice.selected %} class="selected"{% endif %}>
        <a href="{{ i.all_choice.query_string|iriencode }}">
            {{ i.all_choice.display }}
        </a>
    </li>
</ul>
{% endwith %}

Usage:

class MyAdmin(admin.ModelAdmin):
    list_display = [your fields]
    list_filter = [('field 1', SingleTextInputFilter), ('field 2', SingleTextInputFilter), further fields]
Soteriology answered 13/5, 2017 at 2:13 Comment(3)
Thanks, you just need to drop the custom queryset function which looks at a specific imei (telco?) field.Namedropper
Well, no: that function is the one performing the filtering. But you DO need to rename "imei" with your own field name... Actually, I forgot to fix that one for this example ;)Soteriology
you can just fill field name programmatically, like: return queryset.filter(**{self.field.name: self.value()})Circumnavigate
S
2

While it's not actually your question, this sounds like a perfect solution for Django-Selectables you can with just a few lines add an AJAX powered CharField Form that will have it's entries selected from the list of cities. Take a look at the samples listed in the link above.

Squishy answered 3/7, 2011 at 14:18 Comment(2)
This is realy not what I was looking for. My problem is to show working text input filter. Autocomplete feature is nice and I would like to add it later. Anyway, thank you for your response.Evince
ok i figured this on my own. I created my own filter in filterspecs.py (I know that it is nasty way to do it). If you try it this way be carefull about registering your filter. Your filter should be registered before system filters. Than in models.py assign your filter to atribute it belongs. In filter I used something that change posted url where are parameters. Filtering by one city is done by city=Prague but if you want to filter by list of filters you use city__in=Prague,Wien,Dublin. There are many nicer ways how to do this (queries, AJAX,..) but I am just learning.Evince
B
0

Below is the fix for field name..in queryset function

class SingleTextInputFilter(admin.FieldListFilter):
"""
renders filter form with text input and submit button
"""

parameter_name = None
template = "admin/textinput_filter.html"

def __init__(self, field, request, params, model, model_admin, field_path):
    super().__init__(field, request, params, model, model_admin, field_path)
    if self.parameter_name is None:
        self.parameter_name = self.field.name

    if self.parameter_name in params:
        value = params.pop(self.parameter_name)
        self.used_parameters[self.parameter_name] = value

def queryset(self, request, queryset):

    variable_column = self.parameter_name
    search_type = 'icontains'
    filter = variable_column + '__' + search_type

    if self.value():
        return queryset.filter(**{filter: self.value()})
def value(self):
    """
    Returns the value (in string format) provided in the request's
    query string for this filter, if any. If the value wasn't provided then
    returns None.
    """
    return self.used_parameters.get(self.parameter_name, None)

def has_output(self):
    return True

def expected_parameters(self):
    """
    Returns the list of parameter names that are expected from the
    request's query string and that will be used by this filter.
    """
    return [self.parameter_name]

def choices(self, cl):
    all_choice = {
        'selected': self.value() is None,
        'query_string': cl.get_query_string({}, [self.parameter_name]),
        'display': ('All'),
    }
    return ({
        'get_query': cl.params,
        'current_value': self.value(),
        'all_choice': all_choice,
        'parameter_name': self.parameter_name
    }, )
Blather answered 26/3, 2019 at 14:27 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.