Opened 17 years ago

Closed 13 years ago

Last modified 13 years ago

#3400 closed (fixed)

Support for lookup separator with list_filter admin option

Reported by: nick@… Owned by: Simon Meers
Component: contrib.admin Version: newforms-admin
Severity: Keywords: edc nfa-someday list_filter FilterSpec nfa-changelist ep2008
Cc: Simon Litchfield, nick.lane.au@…, remco@…, andreas@…, richard@…, hanne.moa@…, jashugan@…, flosch@…, Gonzalo Saavedra, django@…, Aurelio Tinio, Odin Hørthe Omdal, andy@…, marcoberi@…, ramusus@…, alexey.rudy@…, tjurewicz@…, dan.fairs@…, semente@…, timothy.broder@…, Gabriel Hurley, Simon Meers, osirius@…, Robin Breathe, mmitar@…, cal@…, 3point2, Alexander Koshelev, Mark Jones Triage Stage: Accepted
Has patch: yes Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description

This patch adds support for using the lookup separator in the list_filter option, for example:

class Town(models.Model):
    name = models.CharField()

class House(models.Model):
    town = models.ForeignKey(Town)

class Room(models.Model):
    house = models.ForeignKey(House)

class Booking(models.Model):
    room = models.ForeignKey(Room, raw_id_admin=True)
    
    class Admin:
        list_filter = ('room__house__town',)

Will add a filter "For town:" that spans multiple foreign keys.

Attachments (14)

list_filter.diff (2.1 KB ) - added by nick@… 17 years ago.
Patch for list_filter options
list_filter.2.diff (2.0 KB ) - added by nick@… 17 years ago.
Improved patch
list_filter.3.diff (4.4 KB ) - added by nick.lane.au@… 17 years ago.
Improved patch per suggestions
django-list_filter.diff (4.3 KB ) - added by lucas@… 17 years ago.
Updated diff for new management module
ticket-3400-against-newforms-admin-6477.diff (9.3 KB ) - added by Honza Král 17 years ago.
3400-against-7875.patch (9.2 KB ) - added by Honza Král 16 years ago.
3400-against-8363.patch (9.8 KB ) - added by Vitek Pliska 16 years ago.
3400-against-9646.patch (9.8 KB ) - added by Vitek Pliska 15 years ago.
Quick update for trunk [9646]. Works wih 1.0.2 too
3400-against-10680-with-tests-docs.patch (14.0 KB ) - added by jakub_vysoky 15 years ago.
patch against 10680 with tests and doc
3400-against-10706.diff (14.0 KB ) - added by Honza Král 15 years ago.
new patch with init and some more data in tests
3400-against-10706.2.diff (10.5 KB ) - added by Pavel Szalbot 14 years ago.
works & doesn't create intermediary FilterSpec while traversing model hierarchy
3400.diff (26.3 KB ) - added by Simon Meers 14 years ago.
3400.2.diff (26.3 KB ) - added by Simon Meers 14 years ago.
3400.3.diff (27.2 KB ) - added by Vitek Pliska 13 years ago.
Patch update (against 14662)

Download all attachments as: .zip

Change History (85)

by nick@…, 17 years ago

Attachment: list_filter.diff added

Patch for list_filter options

by nick@…, 17 years ago

Attachment: list_filter.2.diff added

Improved patch

comment:1 by Simon G. <dev@…>, 17 years ago

Keywords: list_filter added
Triage Stage: UnreviewedDesign decision needed

comment:2 by nick@…, 17 years ago

Hmm not sure how I didn't notice before, but this breaks the model validation in django.core.management:

for fn in opts.admin.list_display:
    try:
        f = opts.get_field(fn)
    except models.FieldDoesNotExist:
        if not hasattr(cls, fn):
            e.add(opts, '"admin.list_display" refers to %r, which isn\'t an attribute, method or property.' % fn)
    else:
        if isinstance(f, models.ManyToManyField):
            e.add(opts, '"admin.list_display" doesn\'t support ManyToManyFields (%r).' % fn)

Of course, it shouldn't be hard to fix - but is it worth it? This type of filtering is very useful for my particular apps but if it's not considered common enough for the core there's no point fixing the validation.

comment:3 by Malcolm Tredinnick, 17 years ago

Patch needs improvement: set
Triage Stage: Design decision neededAccepted

I think it's worth getting this right. You probably aren't the only person wanting this functionality.

A couple of comments on the patch:

  1. please create the diff from the top of the Django source tree. I had to guess a lot to work out that it was to be applied to django/contrib/admin/views/main.py (which I think is right). Having the full path from the tree root to the patched file is helpful.
  2. If the FakeForeignKey class has any visibility outside of that function (which I'm not sure about), it shouldn't be declared inside the function. In fact, I'm not really thrilled with declaring it inside the function in the first place, so probably best to move it out to the top-level of the file. I realise you are trying to avoid namespace pollution, but it's not worth the sacrifice of clarity to me.

comment:4 by nick.lane.au@…, 17 years ago

Cc: nick.lane.au@… added
  1. Oops, sorry about the diff - yes that's the correct file.
  2. FakeForeignKey is a wrapper around the ForeignKey field so that the RelatedFilterSpec will work across the foreign keys. Perhaps it could have a better name. It is only instantiated inside get_filters(), but RelatedFilterSpec will have access to it.. so I'll move it to the top-level of the file like you have suggested.

Will update the patch shortly when I have some time, Cheers.

by nick.lane.au@…, 17 years ago

Attachment: list_filter.3.diff added

Improved patch per suggestions

comment:5 by Simon Litchfield <simon@…>, 17 years ago

Works great thanks Nick. This was bugging me just the other day.

comment:6 by cbrand@…, 17 years ago

This is exactly what I need.
I'm going to apply this and try it out.

by lucas@…, 17 years ago

Attachment: django-list_filter.diff added

Updated diff for new management module

comment:7 by Honza Král, 17 years ago

Keywords: newforms-admin FilterSpec added
Version: SVNnewforms-admin

I attached an alternative patch for newforms-admin branch... It can deal with __ paths not just for RelatedFields but for any fields. It will traverse the path, return the last field in the chain and supply additional parameter field_path that is used for filtering the results.

With this you can filter for example by a DateField stored on a related model.

comment:8 by Honza Král, 16 years ago

Also see #5833 for a way to enable users to register their own filter.

comment:9 by Brian Rosner, 16 years ago

Keywords: nfa-someday added; newforms-admin removed

This is a feature enhancement and not critical to the merge of newforms-admin into trunk. Tagging with nfa-someday. I would like to see this functionality get in. After newforms-admin gets merged I will look into this more and examine the patches present.

comment:10 by Ales Zoulek, 16 years ago

Keywords: nfa-changelist added

comment:11 by Remco Wendt, 16 years ago

Cc: remco@… added

by Honza Král, 16 years ago

Attachment: 3400-against-7875.patch added

comment:12 by Vitek Pliska, 16 years ago

Keywords: ep2008 added

I checked and applied 3400-against-7875.patch, all tests passed.

comment:13 by anonymous, 16 years ago

Just wrote small app for testing this. I don't want to write tests.
models.py:

from django.conf import settings
from django.db import models
from django.contrib import admin

class Town(models.Model):
    name = models.CharField(max_length=64)
    
    def __unicode__(self):
        return self.name

class House(models.Model):
    town = models.ForeignKey(Town)

    def __unicode__(self):
        return self.town.name

class Room(models.Model):
    house = models.ForeignKey(House)

    def __unicode__(self):
        return self.house.town.name

class Booking(models.Model):
    room = models.ForeignKey(Room)

    def __unicode__(self):
        return self.room.house.town.name
    
class BookingOpts(admin.ModelAdmin):
    list_filter = ('room__house__town',)
    raw_id_admin = ('room', )

admin.site.register(Town)
admin.site.register(House)
admin.site.register(Room)
admin.site.register(Booking, BookingOpts)

Works fine.

comment:14 by anonymous, 16 years ago

I've applied 3400-against-7875.patch to today svn django-trunk and I get the following error:

Error while importing URLconf 'myapp.urls': ProjectAdmin.list_filter[8] refers to field technology__programing_languages that is missing from model Project.

The models are:

class Project(models.Model):

technology = models.ForeignKey("ProjectTechnology", verbose_name=_('project technology'),unique=True, null=True, blank=True, editable=False, related_name="technology")
...

class ProjectTechnology(models.Model):

programing_languages = models.ManyToManyField(ProgramingLanguage, null=True, blank=True)
...

class ProgramingLanguage(models.Model):

name = models.CharField(_('name'), max_length=200)
...

I'm almost a newbie, what it could be?

Thanks.

by Vitek Pliska, 16 years ago

Attachment: 3400-against-8363.patch added

comment:15 by anonymous, 16 years ago

Thanks, 3400-against-8363.patch solves the problem.

Should it be possible now to show attributes (or foreignkeys) of other model B that have a foreignkey to model A in model A list_filter?, how?

If I should make my question to other sites, don't complaint to tell me.

Thanks in advance.

comment:16 by Andreas Pelme, 16 years ago

Cc: andreas@… added

comment:17 by anonymous, 16 years ago

Path 3400-against-8363.patch does not run against revision 9285.

Is there a way to use lookup separator without patch django?

comment:18 by cornbread, 15 years ago

Does this patch include a way to add sort_by as well?

comment:19 by cornbread, 15 years ago

Cc: richard@… added

This needs a sort_by option as well:

class Town(models.Model):
    name = models.CharField()

class House(models.Model):
    town = models.ForeignKey(Town)

class Room(models.Model):
    house = models.ForeignKey(House)

class Booking(models.Model):
    room = models.ForeignKey(Room, raw_id_admin=True)
    
    class Admin:
        sort_by = ('room__house__town',)

comment:20 by HM, 15 years ago

Cc: hanne.moa@… added

comment:21 by jashugan, 15 years ago

Cc: jashugan@… added

comment:22 by flosch, 15 years ago

Cc: flosch@… added

by Vitek Pliska, 15 years ago

Attachment: 3400-against-9646.patch added

Quick update for trunk [9646]. Works wih 1.0.2 too

comment:23 by Almad, 15 years ago

Any improvements needed for this patch?

Is there any chance this can make it in 1.1.?

comment:24 by mrts, 15 years ago

milestone: 1.2

Tentatively targeting for 1.2.

comment:25 by Gonzalo Saavedra, 15 years ago

Cc: Gonzalo Saavedra added

comment:26 by mrts, 15 years ago

See also #10743.

by jakub_vysoky, 15 years ago

patch against 10680 with tests and doc

comment:27 by jakub_vysoky, 15 years ago

I added patch against newest django trunk version.

main repo lives in http://github.com/HonzaKral/django/tree/ticket3400

comment:28 by jakub_vysoky, 15 years ago

Keywords: edc added
Owner: changed from nobody to jakub_vysoky

by Honza Král, 15 years ago

Attachment: 3400-against-10706.diff added

new patch with init and some more data in tests

comment:29 by francois@…, 15 years ago

Needs tests: set

I feel stupid posting this, but are we sure it works with related_of_related as in the example above?

Using :

from django.db import models
class Town(models.Model):
    name = models.CharField(max_length=50)

class House(models.Model):
    town = models.ForeignKey(Town)

class Room(models.Model):
    house = models.ForeignKey(House)

class Booking(models.Model):
    room = models.ForeignKey(Room)

and

from django.contrib import  admin
from content.models import *

class BookingAdmin(admin.ModelAdmin):
    list_filter = ('room__house__town',)
    pass

admin.site.register(Booking,BookingAdmin)
admin.site.register(Town)
admin.site.register(Room)
admin.site.register(House)

I get a exception in /django/contrib/admin/views/main.py circa line 85


                if '__' in filter_name:
                    f = None
                    model = self.model
                    path = filter_name.split('__')
                    for field_name in path[:-1]:
                        f = model._meta.get_field(field_name)
                        model = f.rel.to
                        f = model._meta.get_field(path[-1]) # !!!! Exception
                        spec = FilterSpec.create(f, request, self.params, model, self.model_admin, field_path=filter_name)
                else:
                    f = lookup_opts.get_field(filter_name)
                    spec = FilterSpec.create(f, request, self.params, self.model, self.model_admin)

as it tries to get a field for path[2] (town) on model path[0] (room) on the first iteration.

I get that problem using the patch 3400-against-9646.patch, which patched all right against 1.0.2-final. That bit code looks identical on subsequent patches, though.

comment:30 by francois@…, 15 years ago

solved it by

                for field_name in path[:-1]:
                        f = model._meta.get_field(field_name)
                        model = f.rel.to
                        f = model._meta.get_field(path[path.index(field_name)+1])

doesn't look very robust but works so far.. Needs more testing, I guess

comment:31 by Pavel Szalbot, 15 years ago

Working and little bit better solution - not creating intermediary FilterSpec(s):

                    for field_index in xrange(len(path)-1):
                        f = model._meta.get_field(path[field_index])
                        model = f.rel.to
                        f = model._meta.get_field(path[field_index+1])
                    spec = FilterSpec.create(f, request, self.params, model, self.model_admin, field_path=filter_name)

comment:32 by jedie, 15 years ago

Cc: django@… added

comment:33 by Aurelio Tinio, 15 years ago

Cc: Aurelio Tinio added

comment:34 by Odin Hørthe Omdal, 15 years ago

Cc: Odin Hørthe Omdal added

comment:35 by Andy Baker, 15 years ago

Cc: andy@… added

comment:36 by marcob, 14 years ago

Cc: marcoberi@… added

comment:37 by Ben Davis, 14 years ago

Patch currently does not work with reverse relationships. For example:

class Target(models.Model):
    name = CharField(max_length=200)

class Survey(models.Model):
    target = models.OneToOneField(Target)
    source = models.ForeignKey(SurveySource)

class TargetAdmin(admin.ModelAdmin):
    list_filter = ['survey__source']
    

My hunch is that this only breaks in the validation code -- I'm investigating right now if that will fix it. The only question is how to correctly traverse reverse relationships using ._meta

comment:38 by anonymous, 14 years ago

Cc: ramusus@… added

comment:39 by anonymous, 14 years ago

Cc: alexey.rudy@… added

comment:40 by trent, 14 years ago

Cc: tjurewicz@… added

comment:41 by Dan Fairs, 14 years ago

Cc: dan.fairs@… added

comment:42 by anonymous, 14 years ago

Cc: semente@… added

by Pavel Szalbot, 14 years ago

Attachment: 3400-against-10706.2.diff added

works & doesn't create intermediary FilterSpec while traversing model hierarchy

comment:43 by Chris Beaven, 14 years ago

milestone: 1.21.3
Summary: [patch] Support for lookup separator with list_filter admin optionSupport for lookup separator with list_filter admin option

Definitely isn't going to make it into 1.2.

comment:44 by marcob, 14 years ago

@SmileyChris: may We do something to make it into 1.2 ? I'm not lobbying :-) With something I meaned some real work

comment:45 by Chris Beaven, 14 years ago

No chance. Since 1.2 beta has been released, we've hit a full feature freeze (and this is definitely a feature).

comment:46 by brentp, 14 years ago

current patch does not respect limit_choices_to when specified for a ForeignKey.

comment:47 by broderboy, 14 years ago

Cc: timothy.broder@… added

comment:48 by anonymous, 14 years ago

Cc: Gabriel Hurley added

comment:49 by Simon Meers, 14 years ago

Cc: Simon Meers added

comment:50 by Sam Bull, 14 years ago

Cc: osirius@… added

comment:51 by Robin Breathe, 14 years ago

Cc: Robin Breathe added

comment:52 by frans, 14 years ago

good point by brentp. I've tried to have a crack at it but got lost in the the bowels of limit_choices_to. If anyone who knows the innards can sketch an approach, I don't mind tackling the actual patching/testing.

comment:53 by Mitar, 14 years ago

Cc: mmitar@… added

comment:54 by Mitar, 14 years ago

Reverse relationship support would be really great as then it would be possible to extend list_filter for UserAdmin with fields from profile. Profile have OneToOneField or ForeignField field defined which point back to django.contrib.auth.models.User model and currently it is not possible to extend admin to filter by fields in the profile. (While it is possible to extend searchable fields.)

comment:55 by Simon Meers, 14 years ago

I have written a patch which works for reverse relationships also; anything you can put in search__fields now works for list__filter also. Just tidying up implementation, limit_choices_to, etc, will upload shortly.

comment:56 by Cal Leeming, 14 years ago

Cc: cal@… added

Hello,

Can DrMeers please upload his most recent patch please?

Cheers

Cal

comment:57 by Simon Meers, 14 years ago

Needs tests: unset
Owner: changed from jakub_vysoky to Simon Meers
Patch needs improvement: unset
Status: newassigned

OK, here is my draft patch. I have not tested it thoroughly; there may still be a few issues to smooth out, so test-drivers would be appreciated.

A few notes:

  • Reverse relationships work
  • You can now use any field path that search_filters will accept
  • I have removed the strict validation in favour of letting setup_joins catch any issues, as search_fields does. We could perhaps look at adding admin validation for field paths for both instead.
  • I played around with combining multiple limit_choices_to clauses based on the models in the field path, however discovered that in practise it really doesn't make sense to apply any before the final model in the field path
  • limit_choices_to is respected for AllValuesFilterSpec, and a utility function is provided if other filterspecs wish to do likewise
  • There are some basic tests included in the patch

by Simon Meers, 14 years ago

Attachment: 3400.diff added

comment:58 by frans, 14 years ago

after testing this patch on my dev/test instance successfully, I'll deploy on my prod server and report

comment:59 by 3point2, 14 years ago

Cc: 3point2 added

comment:60 by frans, 14 years ago

all good in prod for me so far... Will update if I notice anything out of the ordinary

comment:61 by Cal Leeming, 14 years ago

Needs tests: set

Deployed to dev and all looks fine so far.. will let you guys know if anything bad happens. Thanks dr meers!

comment:62 by Simon Meers, 14 years ago

Needs tests: unset

Did you not notice the included tests? If there is a particular aspect you thought was untested, please let me know and I'll add it.

comment:63 by Cal Leeming, 14 years ago

Hi DrMeers,

I actually had problems merging the tests in, the patch file said the path specified for the tests was not valid, but it worked on the other files, so I marked it as "needs tests" but I prob should have marked it as patch needs improvement.

Also, I'm getting problems now in production, which I will attempt to fix later:

Environment:

Request Method: GET
Request URL: http://dev.app.textrecommend.co.uk/admin/mvoucher/vouchersms/
Django Version: 1.2 beta 1
Python Version: 2.6.5
Installed Applications:
['webapp.mvoucher',

'webapp.djangoextend',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.sites',
'django.contrib.admin']

Installed Middleware:
('django.middleware.common.CommonMiddleware',

'django.contrib.sessions.middleware.SessionMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'webapp.djangoextend.djangologging.middleware.SuppressLoggingOnAjaxRequestsMiddleware',
'webapp.djangoextend.djangologging.middleware.LoggingMiddleware',
'webapp.djangoextend.core.GlobalRequestMiddleware',
'webapp.djangoextend.middleware.DjangoExtendMiddleware')

Traceback:
File "/home/textrecommend/local/lib/python2.6/site-packages/django/core/handlers/base.py" in get_response

  1. response = callback(request, *callback_args, callback_kwargs)

File "/home/textrecommend/local/lib/python2.6/site-packages/django/contrib/admin/options.py" in wrapper

  1. return self.admin_site.admin_view(view)(*args, kwargs)

File "/home/textrecommend/local/lib/python2.6/site-packages/django/utils/decorators.py" in _wrapped_view

  1. response = view_func(request, *args, kwargs)

File "/home/textrecommend/local/lib/python2.6/site-packages/django/views/decorators/cache.py" in _wrapped_view_func

  1. response = view_func(request, *args, kwargs)

File "/home/textrecommend/local/lib/python2.6/site-packages/django/contrib/admin/sites.py" in inner

  1. return view(request, *args, kwargs)

File "/home/textrecommend/local/lib/python2.6/site-packages/django/utils/decorators.py" in _wrapper

  1. return decorator(bound_func)(*args, kwargs)

File "/home/textrecommend/local/lib/python2.6/site-packages/django/utils/decorators.py" in _wrapped_view

  1. response = view_func(request, *args, kwargs)

File "/home/textrecommend/local/lib/python2.6/site-packages/django/utils/decorators.py" in bound_func

  1. return func(self, *args2, kwargs2)

File "/home/textrecommend/local/lib/python2.6/site-packages/django/contrib/admin/options.py" in changelist_view

  1. self.date_hierarchy, self.search_fields, self.list_select_related, self.list_per_page, self.list_editable, self)

File "/home/textrecommend/local/lib/python2.6/site-packages/django/contrib/admin/views/main.py" in init

  1. self.filter_specs, self.has_filters = self.get_filters(request)

File "/home/textrecommend/local/lib/python2.6/site-packages/django/contrib/admin/views/main.py" in get_filters

  1. field_path=filter_name)

File "/home/textrecommend/local/lib/python2.6/site-packages/django/contrib/admin/filterspecs.py" in create

  1. field_path=field_path)

File "/home/textrecommend/local/lib/python2.6/site-packages/django/contrib/admin/filterspecs.py" in init

  1. limit_choices_to = get_limit_choices_to_from_path(model, field_path)

File "/home/textrecommend/local/lib/python2.6/site-packages/django/contrib/admin/util.py" in get_limit_choices_to_from_path

  1. hasattr(fields[-1], 'rel') and

Exception Type: IndexError at /admin/mvoucher/vouchersms/
Exception Value: list index out of range

/home/textrecommend/local/lib/python2.6/site-packages/django/contrib/admin/util.py in get_limit_choices_to_from_path

hasattr(fields[-1], 'rel') and ...

▼ Local vars
Variable Value
fields
[]
model
<class 'webapp.mvoucher.models.VoucherSMS'>
path
'carrier_status_code'

# If the carrier returns a status code, this is placed into here.
carrier_status_code = models.CharField(max_length=64, blank=True, null=True)

comment:64 by Cal Leeming, 14 years ago

Patch needs improvement: set

I fixed this by changing:

django/contrib/admin/util.py

def get_limit_choices_to_from_path(model, path):
    """ Return Q object for limiting choices if applicable.

    If final model in path is linked via a ForeignKey or ManyToManyField which
    has a `limit_choices_to` attribute, return it as a Q object.
    """
    fields = get_fields_from_path(model, path)
    fields = remove_trailing_data_field(fields)
+    if len(fields)==0:
+        return models.Q()
    limit_choices_to = (
        hasattr(fields[-1], 'rel') and
        getattr(fields[-1].rel, 'limit_choices_to', None))
    if not limit_choices_to:
        return models.Q() # empty Q
    elif isinstance(limit_choices_to, models.Q):
        return limit_choices_to # already a Q
    else:
        return models.Q(**limit_choices_to) # convert dict to Q

by Simon Meers, 14 years ago

Attachment: 3400.2.diff added

comment:65 by Simon Meers, 14 years ago

Patch needs improvement: unset

Thanks foxwhipser, patch updated.

comment:66 by Alexander Koshelev, 14 years ago

Cc: Alexander Koshelev added

comment:67 by m4rt0g1, 14 years ago

can anyone mail me about which file that needed to be change and full example for doing this code?

comment:68 by Simon Litchfield, 13 years ago

Cc: Simon Litchfield added

comment:69 by Mark Jones, 13 years ago

Cc: Mark Jones added

by Vitek Pliska, 13 years ago

Attachment: 3400.3.diff added

Patch update (against 14662)

comment:70 by Honza Král, 13 years ago

Resolution: fixed
Status: assignedclosed

(In [14674]) Fixed #3400 -- Support for lookup separator with list_filter admin option. Thanks to DrMeers and vitek_pliska for the patch!

comment:71 by Jacob, 13 years ago

milestone: 1.3

Milestone 1.3 deleted

Note: See TracTickets for help on using tickets.
Back to Top