django admin画面に複数選択可能なカスタムリストフィルターを追加する


django admin画面のフィルター機能に不満があるので、 複数選択可能なListFilterを作成します。

modelを準備する

以下のようなmodelを準備します。
商品マスタ product
作家マスタ author
出版社マスタ publisher
class Product(models.Model):
    product_id = models.BigAutoField(
        primary_key=True,
        verbose_name='商品マスタID'
    )
    product_name = models.TextField(
        verbose_name='商品名'
    )
    product_price = models.IntegerField(
        verbose_name='商品価格',
    )
    author = models.ForeignKey(
      Author,
      on_delete=models.PROTECT
    )
    publisher = models.ForeignKey(
      Publisher,
      on_delete=models.PROTECT
    )

    class Meta:
      managed = False
      db_table = 'product'
      verbose_name = '商品マスタ'
      verbose_name_plural = '商品マスタ'

    def __str__(self):
        return self.product_name

class Publisher(models.Model):
    publisher_id = models.BigAutoField(
        primary_key=True,
        verbose_name='出版社マスタID'
    )
    publisher_name = models.TextField(
        verbose_name='出版社名'
    )

    class Meta:
      managed = False
      db_table = 'publisher'
      verbose_name = '出版社マスタ'
      verbose_name_plural = '出版社マスタ'

    def __str__(self):
        return self.publisher_name

class Author(models.Model):
    author_id = models.BigAutoField(
        primary_key=True,
        verbose_name='作家マスタID'
    )
    author_name = models.TextField(
        verbose_name='作家名'
    )

    class Meta:
      managed = False
      db_table = 'author'
      verbose_name = '作家マスタ'
      verbose_name_plural = '作家マスタ'

    def __str__(self):
        return self.author_name

商品マスタを管理画面で呼び出せるようにadmin.pyに以下のように記述します

@admin.register(Product)
class ProductAdmin(admin.ModelAdmin):
    list_display = (
        'product_id',
        'product_name',
        'product_price',
        'author',
        'publisher',
    )
    list_display_links = (
        'product_name',
    )
    list_filter = (
        'author',
        'publisher',
    )
    search_fields = (
        'product_name',
    )
    ordering = (
        'product_id',
    )
これで商品マスタ product が表示できるようになりました。

新たにコミックや小説などのカテゴリを記録するためのカテゴリマスタを作成します

class Category(models.Model):
    category_id = models.BigAutoField(
        primary_key=True,
        verbose_name='カテゴリマスタID'
    )
    category_name = models.TextField(
        verbose_name='カテゴリ名'
    )

    class Meta:
      managed = False
      db_table = 'category'
      verbose_name = 'カテゴリマスタ'
      verbose_name_plural = 'カテゴリマスタ'

    def __str__(self):
        return self.category_name
商品マスタ product にも外部キーでカテゴリマスタを設定します。
  category = models.ForeignKey(
    Category,
    on_delete=models.PROTECT
  )
これでカテゴリも表示できるようになりました。
admin.pyに追加すれが表示できるのですが、カテゴリは複数選択できるようにしたいとおもいました。
そこで複数選択可能なカスタムリストフィルターを作成します。

複数選択可能なカスタムリストフィルター作成する

admin.pyに ListFilter を継承した複数選択可能なカスタムリストフィルターを作成します。
  class MultiSelectListFilter(admin.ListFilter):
    """
    複数選択可能なリストフィルター
    admin の ListFilter を継承しています
    """
    title = '' # 項目として表示される名称
    model = '' # 使用するmodel名
    parameter_name = '' # リクエストのパラメータ名
    field_name = '' # 使用するmodelのフィールド名

    def __init__(self, request, params, model, model_admin):
        # ListFilter を継承
        super().__init__(request, params, model, model_admin)
        # 複数選択にしたいのでリクエスト値を in にします
        self.lookup_kwarg = '%s__in' % self.parameter_name
        # ここらへんは SimpleListFilter と同じ
        if self.lookup_kwarg in params:
            value = params.pop(self.lookup_kwarg)
            self.used_parameters[self.lookup_kwarg] = value
        lookup_choices = self.lookups(request, model_admin)
        if lookup_choices is None:
            lookup_choices = ()
        self.lookup_choices = list(lookup_choices)

    def has_output(self):
        # SimpleListFilter と同じ
        return len(self.lookup_choices) > 0

    def value(self):
        # SimpleListFilter と同じ
        return self.used_parameters.get(self.lookup_kwarg)

    def expected_parameters(self):
        # SimpleListFilter と同じ
        return [self.lookup_kwarg]

    def lookups(self, request, model_admin):
        ids = {}
        if self.value():
            # リクエスト値がある場合、選択済みの値を除外したtupleを返す
            for v in self.model.objects.all():
                current = self.value().split(',')
                parameter_name = str(getattr(v, self.parameter_name))
                if parameter_name in current:
                    current.remove(parameter_name)
                else:
                    current.append(parameter_name)
                str_ids = ','.join(list(current))
                ids.update({str_ids: getattr(v, self.field_name)})
            return tuple(ids.items())
        else:
            # デフォルトは全件表示。パラメータ名、フィールド名のtupleを返す
            return self.model.objects.all().values_list(
                self.parameter_name,
                self.field_name
            )

    def choices(self, changelist):
        # 全て を表示
        yield {
            'selected': self.value() is None,
            'query_string': changelist.get_query_string(
                remove=[self.parameter_name]
            ),
            'display': _('All'),
        }
        # 選択済みの値はselectedで表示する。この判定のためにis_match関数を作成している
        for lookup, title in self.lookup_choices:
            yield {
                'selected': self.is_match(lookup, self.value()),
                'query_string': changelist.get_query_string(
                    {self.lookup_kwarg: lookup}
                ),
                'display': title,
            }

    def queryset(self, request, queryset):
        if self.value():
            # 選択されたデータで絞り込んで表示
            query = {
                self.lookup_kwarg: self.value().split(','),
            }
            return queryset.filter(**query)
        else:
            # デフォルトの表示
            return queryset.all()

    @staticmethod
    def is_match(lookup: str, value: str):
        """
        選択済みの値とリクエストの値が一致しているか判定する
        :param lookup: 選択済みの値
        :param value: リクエストの値
        :return:
        """
        if not lookup:
            return True
        if not value:
            return False
        if all(v in lookup.split(',') for v in value.split(',')):
            return False
        else:
            return True
admin.py に カテゴリマスタとカスタムリストフィルターでの表示を記載します。
@admin.register(Product)
class ProductAdmin(admin.ModelAdmin):
    class CategoryListFilter(MultiSelectListFilter):
        """
        カテゴリマスタのカスタムフィルターの設定
        """
        title = 'カテゴリ'
        model = Category
        parameter_name = 'category_id'
        field_name = 'category_name'

    list_display = (
        'product_id',
        'product_name',
        'product_price',
        'author',
        'publisher',
        'category', # カテゴリマスタの追加
    )
    list_display_links = (
        'product_name',
    )
    list_filter = (
        'author',
        'publisher',
        CategoryListFilter, # カテゴリマスタのリストフィルターの追加
    )
    search_fields = (
        'product_name',
    )
    ordering = (
        'product_id',
    )
商品マスタの表示から「小説」「文庫」を選択
これで複数選択の絞り込みができるようになりました。
SimpleListFilter が便利なのですが、ちょっと痒いところに手が届かなかったので作成しました。
現在の評価: 1

コメント

コメントを投稿
コメントするには TORICO-ID にログインしてください。
ログイン コメント利用規約