2012-08-31 10 views
41

Mam aplikacji django, który ma dwa modele tak:Czy mogę zrobić list_filter w django admin, aby wyświetlać tylko odnośniki ForeignKeys?

class MyModel(models.Model): 
    name = models.CharField() 
    country = models.ForeignKey('Country') 

class Country(models.Model): 
    code2 = models.CharField(max_length=2, primary_key=True) 
    name = models.CharField() 

Klasa admin MyModel wygląda następująco:

class MyModelAdmin(admin.ModelAdmin): 
    list_display = ('name', 'country',) 
    list_filter = ('country',) 
admin.site.register(models.MyModel, MyModelAdmin) 

Country tabela zawiera ~ 250 krajach. Tylko kilka krajów jest rzeczywiście przywoływanych przez pewną instancję MyModel.

Problem polega na tym, że filtr listy w django admin zawiera listę WSZYSTKICH krajów w panelu filtra. Lista wszystkich krajów (a nie tylko tych, do których odwołuje się instancja) w dużym stopniu pogarsza cel posiadania filtra listy w tym przypadku.

Czy niektóre kraje mogą wyświetlać tylko kraje oznaczone przez MyModel jako opcje w filtrze listy? (Używam Django 1.3.)

Odpowiedz

58

Od Django 1.8, jest wbudowany RelatedOnlyFieldListFilter, którego można użyć do pokazania pokrewnych krajów.

class MyModelAdmin(admin.ModelAdmin): 
    list_display = ('name', 'country',) 
    list_filter = (
     ('country', admin.RelatedOnlyFieldListFilter), 
    ) 

Dla Django 1,4-1,7, list_filter pozwala na użycie podklasę SimpleListFilter. Powinno być możliwe utworzenie prostego filtru listy zawierającego listę żądanych wartości.

Jeśli nie możesz przeprowadzić aktualizacji z Django 1.3, musisz użyć wewnętrznego i nieudokumentowanego aplika- cji FilterSpec. Pytanie przepełnienia stosu Custom Filter in Django Admin powinno skierować cię we właściwym kierunku.

+0

Dzięki za odpowiedź. Migracja do Django 1.4 jest planowana na najbliższą przyszłość, więc odłożę wszelkie poprawki do problemu do tego czasu. – m000

+1

Od "1.8" ... http://stackoverflow.com/a/27836981/953553 – andi

+0

@i dziękuję, zaktualizowałem odpowiedź o nowe informacje – Alasdair

30

Wiem, że pytanie dotyczyło Django 1.3, ale wspomniałeś o tym, że wkrótce przejdziesz na wersję 1.4. Również dla osób, takich jak ja, które szukały rozwiązania dla wersji 1.4, ale znalazłem ten wpis, postanowiłem pokazać pełny przykład użycia SimpleListFilter (dostępny Django 1.4) do wyświetlenia tylko odnośników (powiązanych, używanych) wartości kluczy obcych

from django.contrib.admin import SimpleListFilter 

# admin.py 
class CountryFilter(SimpleListFilter): 
    title = 'country' # or use _('country') for translated title 
    parameter_name = 'country' 

    def lookups(self, request, model_admin): 
     countries = set([c.country for c in model_admin.model.objects.all()]) 
     return [(c.id, c.name) for c in countries] 
     # You can also use hardcoded model name like "Country" instead of 
     # "model_admin.model" if this is not direct foreign key filter 

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

# Example setup and usage 

# models.py 
from django.db import models 

class Country(models.Model): 
    name = models.CharField(max_length=64) 

class City(models.Model): 
    name = models.CharField(max_length=64) 
    country = models.ForeignKey(Country) 

# admin.py 
from django.contrib.admin import ModelAdmin 

class CityAdmin(ModelAdmin): 
    list_filter = (CountryFilter,) 

admin.site.register(City, CityAdmin) 

W przykładzie można zobaczyć dwa modele - Miasto i kraj. Miasto ma ForeignKey do kraju. Jeśli użyjesz zwykłego list_filter = ("kraj"), będziesz miał wszystkie kraje w selektorze. Ten fragment filtruje jednak tylko kraje pokrewne - te, które mają co najmniej jeden związek z miastem.

Oryginalny pomysł z here. Wielkie dzięki dla autora. Poprawione nazwy klas dla lepszej jasności i użycia metody model_admin.model zamiast nazwy modelu z twardym kodowaniem.

Przykład także dostępny w Django rozszerzone: http://djangosnippets.org/snippets/2885/

+0

Ten przykład jest bardzo zbliżony do tego, czego szukam, ale z jednym wyjątkiem: Próbuję dodać list_filter dla obiektów użytkownika przez pole w modelu UserProfile o nazwie user_type. Tak więc definiuję klasę UserTypeFilter (SimpleListFilter): , ale nie wiem, co umieściłeś w zestawie queryset.filter funkcji zapytania queryset ( = self.value()). –

+0

Odpowiedź na moje pytanie jest tutaj: http://stackoverflow.com/questions/19187027/django-1-4-user-admin-list-filter-using-userprofile-field/19187261?noredirect=1#19187261 –

+0

Jeśli potrzebuję uogólnionej wersji tej wspaniałej odpowiedzi, zobacz moją odpowiedź poniżej: http://stackoverflow.com/a/29501136/304209 –

5

bym zmienić wyszukiwań w kodzie darklow jest tak:

def lookups(self, request, model_admin): 
    users = User.objects.filter(id__in = model_admin.model.objects.all().values_list('user_id', flat = True).distinct()) 
    return [(user.id, unicode(user)) for user in users] 

Jest to o wiele lepsze dla bazy danych;)

18

Ponieważ Django 1 .8 znajduje się: admin.RelatedOnlyFieldListFilter

Przykład użycia:

class BookAdmin(admin.ModelAdmin): 
    list_filter = (
     ('author', admin.RelatedOnlyFieldListFilter), 
    ) 
+1

1.8 przybył i ta funkcja jest już dostępna –

+1

@Rrrrrrrrrrk zaktualizował moją odpowiedź, wyjaśniając obecny stan;) – andi

+0

dla niektórych powód, wydaje się, że to nie jest administrator geodjango, a ściągnięcie go ze zwykłego administratora nie działa – Chozabu

1

@andi, dzięki za pozwalając wiedzieć o tym, że Django 1.8 będą miały tę funkcję.

Sprawdziłem, w jaki sposób zostało ono zaimplementowane i na podstawie tej stworzonej wersji, która działa dla Django 1.7. To jest lepsza implementacja niż moja poprzednia odpowiedź, ponieważ teraz możesz ponownie użyć tego filtra z dowolnym polem klucza obcego. Testowane tylko w Django 1.7, nie wiem, czy działa we wcześniejszych wersjach.

Oto moje ostateczne rozwiązanie:

from django.contrib.admin import RelatedFieldListFilter 

class RelatedOnlyFieldListFilter(RelatedFieldListFilter): 
    def __init__(self, field, request, params, model, model_admin, field_path): 
     super(RelatedOnlyFieldListFilter, self).__init__(
      field, request, params, model, model_admin, field_path) 
     qs = field.related_field.model.objects.filter(
      id__in=model_admin.get_queryset(request).values_list(
       field.name, flat=True).distinct()) 
     self.lookup_choices = [(each.id, unicode(each)) for each in qs] 

Zastosowanie:

class MyAdmin(admin.ModelAdmin): 
    list_filter = (
     ('user', RelatedOnlyFieldListFilter), 
     ('category', RelatedOnlyFieldListFilter), 
     # ... 
    ) 
+0

Niestety, nie działa w Django 1.4 :(Obiekt 'ForeignKey 'nie ma atrybutu" related_field "' –

+0

Dobrze działał na Django 1.6: D –

1

Uogólniony wielokrotnego użytku wersja wielkiego @ darklow za odpowiedź:

def make_RelatedOnlyFieldListFilter(attr_name, filter_title): 

    class RelatedOnlyFieldListFilter(admin.SimpleListFilter): 
     """Filter that shows only referenced options, i.e. options having at least a single object.""" 
     title = filter_title 
     parameter_name = attr_name 

     def lookups(self, request, model_admin): 
      related_objects = set([getattr(obj, attr_name) for obj in model_admin.model.objects.all()]) 
      return [(related_obj.id, unicode(related_obj)) for related_obj in related_objects] 

     def queryset(self, request, queryset): 
      if self.value(): 
       return queryset.filter(**{'%s__id__exact' % attr_name: self.value()}) 
      else: 
       return queryset 

    return RelatedOnlyFieldListFilter 

Zastosowanie:

class CityAdmin(ModelAdmin): 
    list_filter = (
     make_RelatedOnlyFieldListFilter("country", "Country with cities"), 
    ) 
2

To jest moje podejście do ogólnej i wielokrotnego użytku implementacji Django 1.4, jeśli utkniesz w tej wersji. Inspiruje go built-in version that is now part of Django 1.8 i wzwyż. Ponadto, powinno to być dość małe zadanie, aby dostosować go do 1,5-1,7, głównie metody zapytań zmieniły nazwę w tych. Umieściłem filtr w aplikacji core, którą mam, ale możesz oczywiście umieścić ją w dowolnym miejscu.

Realizacja:

# myproject/core/admin/filters.py: 

from django.contrib.admin.filters import RelatedFieldListFilter 


class RelatedOnlyFieldListFilter(RelatedFieldListFilter): 
    def __init__(self, field, request, params, model, model_admin, field_path): 
     self.request = request 
     self.model_admin = model_admin 
     super(RelatedOnlyFieldListFilter, self).__init__(field, request, params, model, model_admin, field_path) 

    def choices(self, cl): 
     limit_choices_to = set(self.model_admin.queryset(self.request).values_list(self.field.name, flat=True)) 
     self.lookup_choices = [(pk_val, val) for pk_val, val in self.lookup_choices if pk_val in limit_choices_to] 
     return super(RelatedOnlyFieldListFilter, self).choices(cl) 

Zastosowanie:

# myapp/admin.py: 

from django.contrib import admin 
from myproject.core.admin.filters import RelatedOnlyFieldListFilter 
from myproject.myapp.models import MyClass 


class MyClassAdmin(admin.ModelAdmin): 
    list_filter = (
     ('myfield', RelatedOnlyFieldListFilter), 
    ) 

admin.site.register(MyClass, MyClassAdmin) 

Jeśli później aktualizować do Django 1.8 powinien być w stanie po prostu zmienić ten import:

from myproject.core.admin.filters import RelatedOnlyFieldListFilter 

Do tego:

from django.contrib.admin.filters import RelatedOnlyFieldListFilter