[PYTHON] Personnalisation de list_filter de l'écran d'administration de Django

Notez que j'ai étudié comment affiner les éléments de list_filter par le résultat d'un autre list_filter.

Chose que tu veux faire

Pour le modèle dans lequel la relation un-à-plusieurs est concaténée, spécifiez le modèle source de concaténation dans list_filter. Pour le moment, je souhaite réduire les éléments de list_filter en fonction des éléments sélectionnés.

Puisqu'il est difficile à comprendre avec des mots, considérez le modèle suivant comme un exemple.

Kobito.R4Pvai.png

Spécifiez le magasin et le personnel comme list_filter sur l'écran de gestion des équipes. Si vous sélectionnez un magasin ici, vous souhaitez restreindre les éléments de sélection du personnel.

Kobito.me0mLo.png

Conclusion

Remplacez la méthode field_choices de ʻadmin.RelatedFieldListFilteret Passez le nom du champ et la classe créée àlist_filter` en tant que taple.

Lors de la substitution, passez l'argument limit_choices_to lors de l'appel de field.get_choices. La valeur à passer est un objet Q ou un dictionnaire.

Affinez-vous une fois au magasin, vérifiez le type de requête émise et affinez-vous par cette valeur.

code

admin.py


from django.contrib import admin
from django.db.models import Q

from .models import Shift


class StaffFieldListFilter(admin.RelatedFieldListFilter):
    def field_choices(self, field, request, model_admin):
        shop_id = request.GET.get('staff__shop__id__exact')
        limit_choices_to = Q(shop_id__exact=shop_id) if shop_id else None
        return field.get_choices(include_blank=False, limit_choices_to=limit_choices_to)


class ShiftAdmin(admin.ModelAdmin):
    list_display = (
        'staff',
        'start_at',
        'end_at',
    )
    list_filter = (
        'staff__shop',
        ('staff', StaffFieldListFilter),
    )

admin.site.register(Shift, ShiftAdmin)

résultat

Kobito.4xD8xK.png

Kobito.lOpw7b.png

Lors de la sélection d'un magasin, le personnel était restreint par le magasin auquel ils appartiennent.

--

Ce que j'ai regardé

Si vous définissez list_filter dans la classe ModelAdmin, les éléments seront créés automatiquement, alors vérifiez cette partie.

Vérifiez le modèle d'affichage

Filtrer par [django / contrib / admin / templates / admin / change_list.html](https://github.com/django/django/blob/44a6c27fd461e1d2f37388c26c629f8f170e8722/django/contrib/admin/templates/admin/change_list.html# Lors de l'examen de la méthode de sortie de Confirmez que la balise de modèle ʻadmin_list_filter` est utilisée.

À propos de la ligne 66


      {% block filters %}
        {% if cl.has_filters %}
          <div id="changelist-filter">
            <h2>{% trans 'Filter' %}</h2>
            {% for spec in cl.filter_specs %}{% admin_list_filter cl spec %}{% endfor %}
          </div>
        {% endif %}
      {% endblock %}

Vérifiez la balise du modèle de sortie

Défini à partir de la ligne 418 de [django / contrib / admin / templatetags / admin_list.py](https://github.com/django/django/blob/44a6c27fd461e1d2f37388c26c629f8f170e8722/django/contrib/admin/templatlist.min. A été découvert. La spécification «choice (cl)» semble être la méthode de création.

Ligne 418


@register.simple_tag
def admin_list_filter(cl, spec):
    tpl = get_template(spec.template)
    return tpl.render({
        'title': spec.title,
        'choices': list(spec.choices(cl)),
        'spec': spec,
    })

Confirmation de l'identité de spécification

Revenez à changelist_view et voyez comment cl.filter_specs est créé.

Passé par près de la ligne 107 de django.contrib.admin.views.main Confirmez que list_filter est inséré dans la spécification. Il semble que les fonctions et les taples peuvent être passés à list_filter en plus des chaînes de caractères. Dans le cas de tapple, il semble être défini comme (field, field_list_filter_class). Dans le cas d'une chaîne de caractères, il semble qu'un filtre approprié soit automatiquement créé. Donc, spec est une instance de field_list_filter_class, donc vérifions la méthode choice de cette classe.

Si vous insérez print (type (spec)) ici, vous pouvez déterminer la classe que vous utilisez.

python:django/contrib/admin/views/main.py_l.107


        if self.list_filter:
            for list_filter in self.list_filter:
                if callable(list_filter):
                    # This is simply a custom list filter class.
                    spec = list_filter(request, lookup_params, self.model, self.model_admin)
                else:
                    field_path = None
                    if isinstance(list_filter, (tuple, list)):
                        # This is a custom FieldListFilter class for a given field.
                        field, field_list_filter_class = list_filter
                    else:
                        # This is simply a field name, so use the default
                        # FieldListFilter class that has been registered for
                        # the type of the given field.
                        field, field_list_filter_class = list_filter, FieldListFilter.create
                    if not isinstance(field, models.Field):
                        field_path = field
                        field = get_fields_from_path(self.model, field_path)[-1]

                    lookup_params_count = len(lookup_params)
                    spec = field_list_filter_class(
                        field, request, lookup_params,
                        self.model, self.model_admin, field_path=field_path
                    )

Méthode RelatedFieldListFilter / choix

J'ai trouvé la méthode du problème dans ligne 197 de django / contrib / admin / filters.py. Il semble que ce soit une méthode génératrice qui émet chaque élément après avoir émis les éléments pour annulation («tous»). L'élément de sélection est self.lookup_choices, donc nous allons ensuite vérifier celui-ci.

python:django/contrib/admin/filters.py_l.197


    def choices(self, changelist):
        yield {
            'selected': self.lookup_val is None and not self.lookup_val_isnull,
            'query_string': changelist.get_query_string(
                {},
                [self.lookup_kwarg, self.lookup_kwarg_isnull]
            ),
            'display': _('All'),
        }
        for pk_val, val in self.lookup_choices:
            yield {
                'selected': self.lookup_val == str(pk_val),
                'query_string': changelist.get_query_string({
                    self.lookup_kwarg: pk_val,
                }, [self.lookup_kwarg_isnull]),
                'display': val,
            }
        if self.include_empty_choice:
            yield {
                'selected': bool(self.lookup_val_isnull),
                'query_string': changelist.get_query_string({
                    self.lookup_kwarg_isnull: 'True',
                }, [self.lookup_kwarg]),
                'display': self.empty_value_display,
            }

Vérifiez self.lookup_choices

Attribué par la méthode init dans la même classe. Le contenu semble être la méthode field_choices dans la même classe.

python:django/contrib/admin/filters.py_l.161


    def __init__(self, field, request, params, model, model_admin, field_path):
        other_model = get_model_from_relation(field)
        self.lookup_kwarg = '%s__%s__exact' % (field_path, field.target_field.name)
        self.lookup_kwarg_isnull = '%s__isnull' % field_path
        self.lookup_val = request.GET.get(self.lookup_kwarg)
        self.lookup_val_isnull = request.GET.get(self.lookup_kwarg_isnull)
        super().__init__(field, request, params, model, model_admin, field_path)
        self.lookup_choices = self.field_choices(field, request, model_admin)

Vérifiez la méthode field_choices

Ceci est également défini dans la même classe. La méthode de champ get_choices est appelée.

python:django/contrib/admin/filters.py_l.194


    def field_choices(self, field, request, model_admin):
        return field.get_choices(include_blank=False)

Ici, insérez à nouveau print (type (field)) et vérifiez la classe du champ. La sortie est la suivante.

<class 'django.db.models.fields.related.ForeignKey'>

Vérifiez la méthode ForeignKey / get_choices

Archiver django / db / models / fields / related.py La définition de la méthode get_choicesn'a pas pu être confirmée. Il semble que la méthode de la classe héritéeField` soit utilisée

Vérifiez la méthode Field / get_choices

Défini à la ligne 783 de django / db / models / fields / __ init__.py Découvert.

python:django/db/models/fields/__init__.py_l.783


    def get_choices(self, include_blank=True, blank_choice=BLANK_CHOICE_DASH, limit_choices_to=None):
        """
        Return choices with a default blank choices included, for use
        as <select> choices for this field.
        """
        blank_defined = False
        choices = list(self.choices) if self.choices else []
        named_groups = choices and isinstance(choices[0][1], (list, tuple))
        if not named_groups:
            for choice, __ in choices:
                if choice in ('', None):
                    blank_defined = True
                    break

        first_choice = (blank_choice if include_blank and
                        not blank_defined else [])
        if self.choices:
            return first_choice + choices
        rel_model = self.remote_field.model
        limit_choices_to = limit_choices_to or self.get_limit_choices_to()
        if hasattr(self.remote_field, 'get_related_field'):
            lst = [(getattr(x, self.remote_field.get_related_field().attname),
                   smart_text(x))
                   for x in rel_model._default_manager.complex_filter(
                       limit_choices_to)]
        else:
            lst = [(x.pk, smart_text(x))
                   for x in rel_model._default_manager.complex_filter(
                       limit_choices_to)]
        return first_choice + lst

Confirmez que si limit_choices_to est défini, une sorte de filtre semble être appliquée par la méthode complex_filter.

Vérifiez complex_filter

Défini dans près de la ligne 855 de django / db / models / query.py.

Il n'y a pas de définition dans la classe Manager, mais l'emplacement de la définition est différent car la classe Manager dispose d'un processus pour appeler la méthode de la classe QuerySet.

python:django/db/models/query.py_l.855


    def complex_filter(self, filter_obj):
        """
        Return a new QuerySet instance with filter_obj added to the filters.
        filter_obj can be a Q object or a dictionary of keyword lookup
        arguments.
        This exists to support framework features such as 'limit_choices_to',
        and usually it will be more natural to use other methods.
        """
        if isinstance(filter_obj, Q):
            clone = self._chain()
            clone.query.add_q(filter_obj)
            return clone
        else:
            return self._filter_or_exclude(None, **filter_obj)

Comme vous pouvez le voir dans les commentaires, vous pouvez passer un objet Q ou un dictionnaire. Alors, je l'ai ajusté pour passer celui-ci.

Recommended Posts

Personnalisation de list_filter de l'écran d'administration de Django
Personnalisation de l'écran d'administration de Django Première étape
Mémo inversé de l'écran de gestion Django
Flux d'ajout d'écran Django2
Réglage du site d'administration de Django
Lorsque vous oubliez le nom d'utilisateur et le mot de passe de l'écran de gestion dans Django
L'histoire de l'échec de la mise à jour de "calendar.day_abbr" sur l'écran d'administration de django
Comment enregistrer une seule donnée sur l'écran de gestion de Django