web-dev-qa-db-fra.com

Puis-je faire list_filter dans Django admin pour afficher uniquement les clés étrangères référencées?

J'ai une application Django qui a deux modèles comme celui-ci:

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()

La classe d'administration pour MyModel ressemble à ceci:

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

Le tableau Country contient ~ 250 pays. Seuls quelques pays sont référencés par une instance MyModel.

Le problème est que le filtre de liste dans le Django admin liste TOUS les pays dans le panneau de filtrage. Liste de tous les pays (et pas seulement ceux qui sont référencés par une instance) défait à peu près le but d'avoir le filtre de liste dans ce cas.

Y en a-t-il pour afficher uniquement les pays référencés par MyModel comme choix dans le filtre de liste? (J'utilise Django 1.3.)

52
m000

Depuis Django 1.8, il existe un RelatedOnlyFieldListFilter intégré, que vous pouvez utiliser pour afficher les pays liés.

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

Pour Django 1.4-1.7, list_filter vous permet d'utiliser une sous-classe de SimpleListFilter. Il devrait être possible de créer un simple filtre de liste qui répertorie les valeurs souhaitées.

Si vous ne pouvez pas mettre à niveau depuis Django 1.3, vous devez utiliser l'api interne et non documenté FilterSpec. La question de dépassement de pile Le filtre personnalisé dans Django Admin devrait vous orienter dans la bonne direction.

72
Alasdair

Je sais que la question portait sur Django 1.3 mais vous l'avez mentionné sur la prochaine mise à niveau vers 1.4. Aussi pour les gens, comme moi qui cherchaient une solution pour 1.4, mais qui ont trouvé cette entrée, j'ai décidé de montrer un exemple complet d'utiliser SimpleListFilter (disponible Django 1.4) pour afficher uniquement les valeurs de clé étrangère référencées (liées, utilisées)

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)

Par exemple, vous pouvez voir deux modèles - Ville et Pays. La ville possède une clé étrangère vers le pays. Si vous utilisez list_filter = ('country',) normal, vous aurez tous les pays dans le sélecteur. Cet extrait ne filtre cependant que les pays liés - ceux qui ont au moins une relation avec la ville.

Idée originale de ici . Un grand merci à l'auteur. Noms de classe améliorés pour une meilleure clarté et utilisation de model_admin.model au lieu du nom de modèle codé en dur.

Exemple également disponible dans Django Snippets: http://djangosnippets.org/snippets/2885/

32
darklow

Depuis Django 1.8 il y a: admin.RelatedOnlyFieldListFilter

L'exemple d'utilisation est:

class BookAdmin(admin.ModelAdmin):
    list_filter = (
        ('author', admin.RelatedOnlyFieldListFilter),
    )
20
andilabs

Je changerais les recherches dans le code de darklow comme ceci:

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]

C'est beaucoup mieux pour la base de données;)

5
user3471924

Ceci est mon point de vue sur une implémentation générale et réutilisable pour Django 1.4, si vous êtes bloqué sur cette version. Elle est inspirée par la version intégrée qui fait maintenant partie de Django 1.8 et plus. De plus, ce devrait être une tâche assez petite de l'adapter à 1.5–1.7, principalement les méthodes de jeu de requêtes ont changé de nom dans celles-ci. se filtrer dans une application core que j'ai mais vous pouvez évidemment le mettre n'importe où.

Implémentation:

# 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)

Utilisation:

# 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)

Si vous effectuez une mise à jour ultérieure vers Django 1.8, vous devriez pouvoir simplement modifier cette importation:

from myproject.core.admin.filters import RelatedOnlyFieldListFilter

Pour ça:

from Django.contrib.admin.filters import RelatedOnlyFieldListFilter

@andi, merci d'avoir fait savoir que Django 1.8 aura cette fonctionnalité.

J'ai regardé comment il a été implémenté et basé sur cette version créée qui fonctionne pour Django 1.7. C'est une meilleure implémentation que ma réponse précédente, car maintenant vous pouvez réutiliser ce filtre avec n'importe quel étranger Champs clés Testé uniquement dans Django 1.7, je ne sais pas si cela fonctionne dans les versions antérieures.

Voici ma dernière solution:

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]

Usage:

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

Une version réutilisable généralisée de la réponse du grand @ darklow:

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

Usage:

class CityAdmin(ModelAdmin):
    list_filter = (
        make_RelatedOnlyFieldListFilter("country", "Country with cities"),
    )
1
Dennis Golomazov