15

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.

Jazzuell
  • 253
  • 1
  • 2
  • 8

4 Answers4

21

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.

funky-future
  • 3,716
  • 1
  • 30
  • 43
r_black
  • 614
  • 7
  • 10
  • 1
    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. – devsnd Nov 13 '14 at 13:46
  • @devsnd 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 https://github.com/django/django/blob/master/django/contrib/admin/views/main.py#L325 if filter returns None nothing happens. – r_black Nov 26 '14 at 13:33
  • 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. – devsnd Nov 26 '14 at 13:39
  • 1
    @devsnd 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. – r_black Nov 26 '14 at 14:47
  • I've been looking for this solution for 2 months. Thank you very much kind sir! – forbdn Dec 28 '16 at 17:27
  • 3
    I found [this article with another solution that worked for me](https://hakibenita.com/how-to-add-a-text-filter-to-django-admin) on Django 3.2. – kjpc-tech Mar 14 '22 at 18:51
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]
Community
  • 1
  • 1
velis
  • 8,747
  • 4
  • 44
  • 64
  • Thanks, you just need to drop the custom queryset function which looks at a specific imei (telco?) field. – Semprini May 09 '18 at 01:44
  • 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 ;) – velis May 09 '18 at 05:32
  • 1
    you can just fill field name programmatically, like: `return queryset.filter(**{self.field.name: self.value()})` – jakubste Sep 20 '19 at 14:52
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.

Doug-W
  • 1,318
  • 12
  • 14
  • 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. – Jazzuell Jul 03 '11 at 14:56
  • 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. – Jazzuell Aug 18 '11 at 23:12
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
    }, )