【Python】Djangoのクラスベース汎用ビュー【ListView】

# はじめに
Djangoは「クラスベース汎用ビュー」というクラスを使ったビューが用意されていて、少ないコードで豊富な機能を実装することができます。その一方でコードが内部に隠れてしまうので、仕組みが分かりにくいというデメリットもあります。

詳細は私もよく分からないのですが、カスタマイズの方法を記録として残しておきたいと思います。

今回は<strong>generic.ListView</strong>です。

# ※補足
いつもはdjangoプロジェクトの作成から始めていますが、今回は割愛します。というのも、今回実装する機能は以前の記事[「DjangoのCRUD処理について」](https://qiita.com/grv2688/items/aaf70aee236add63f0d0)をクラスベース汎用ビューに置き換えたものなので、詳細はそちらをご覧頂ければ幸いです。

# イメージ
ListViewを使って、モデルを取得し、一覧で表示します。

![スクリーンショット 2021-04-25 22.15.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689015/4f752304-bfa5-4fa1-aa32-deb1bad6b034.png)

# モデル
モデルはこのような感じです。

```python:models.py
from django.db import models
from django.utils import timezone


class ReviewByClass(models.Model):
    STARS = (
        (1, '★'),
        (2, '★★'),
        (3, '★★★'),
        (4, '★★★★'),
        (5, '★★★★★'),
    )

    store_name = models.CharField('store_name', max_length=255)
    title = models.TextField('title', max_length=255)
    text = models.TextField('review', blank=True)
    stars = models.IntegerField('stars', choices=STARS)
    created_at = models.DateTimeField('created_at', default=timezone.now)

    def __str__(self):
        return self.title
```

# ビュー
肝心のListViewは以下のような感じです。クラスはたったの2行です。

ご覧の通り、テンプレートファイルの名称さえ書かなくても機能してしまいます。便利な反面、どうやってカスタマイズしたら良いか分かりにくいです。

```python:views.py
from .models import ReviewByClass
from django.views import generic # クラスベース汎用ビュー


class ReviewList(generic.ListView):
    model = ReviewByClass
```

# テンプレートファイル
テンプレートファイルは以下の様になります。

```html:reviewcbyclass_list.html
{% extends "base.html" %}

<!-- コンテンツ -->
{% block content %}
<div class="container">
  <h1>Reviews By Class</h1>
  <hr>
  {% for reviewbyclass in reviewbyclass_list %}
    <article>
      <h3><a href="{% url 'reviewsByClass:reviewbyclass_detail' reviewbyclass.pk %}">{{ reviewbyclass.store_name }}</a></h3>
      <p>{{ reviewbyclass.get_stars_display }} - {{ reviewbyclass.title }} - {{ reviewbyclass.created_at }}</p>
    </article>
  {% endfor %}
  <a href="{% url 'reviewsByClass:reviewbyclass_create' %}">create review</a>
</div>
{% endblock %}
```

# 仕様: テンプレートファイルの名称
ListViewの場合、デフォルトで使用されるテンプレートファイルは<strong>「モデル名小文字_list.html」</strong>になります。

# 仕様: QuerySetの名称
テンプレートファイルに渡されるQuerySet（モデルインスタンス）は<strong>「モデル名小文字_list」</strong>または「object_list」になります。

# リストの並び替え
上のテンプレートでリストを取り出すのにfor文を使っていますが、取り出す順番を変えたくなる事があります。取り出すリストの順番を並び替える方法は以下の通りです。

```python:views.py
class ReviewList(generic.ListView)
    model = ReviewClass
    ordering = ('-stars', '-created_at')
    # 別の書き方？
    # queryset = ReviewByClass.objects.order_by('-stars', '-created_at')
```

starsフィールドを降順にした上で、星の数が同じものは作成日(created_at)が新しい投稿を上に表示されます。

get_orderingメソッドを上書きする方法もあります。これによって、昇順、降順を動的に制御することができるみたいです。


```python:views.py
class ReviewList(generic.ListView):
    model = ReviewByClass

    def get_ordering(self):
        a = 10
        if a > 5:
            return ('-stars', 'created_at')
        else:
            return '-creates_at' 
```

# データの受け渡し
views.pyからテンプレートファイルへデータを受け渡す方法は以下の通りです。

```python:views.py
class ReviewList(generic.ListView):
    model = ReviewByClass
    ordering = ('-stars','-created_at')
    extra_context = {'KEY': 'VALUE!!'}
```

get_context_dataメソッドを使用する方法もあります。こちらもget_orderingメソッドと同様にif文などと組み合わせる事ができます。

```python:views.py
class ReviewList(generic.ListView):
    model = ReviewByClass
    ordering = ('-stars','-created_at')
    
    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['KEY'] = 'VALUE!!'
        return context
```

テンプレートファイル側の受け方は以下の通りです。

```html:reviewbyclass_list.html
  <h1>Reviews By Class</h1>
  <h1>{{ KEY }}</h1>
  <hr>
  <!-- 以下省略 -->
```

画面は以下のようになります。

![スクリーンショット 2021-04-25 22.43.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689015/16f9bfad-55a7-c25d-8218-1f5a73237709.png)

# QuerySetの絞り込み
テンプレートファイルへ渡すQuerySetを絞り込むこともできます。例えば、検索フォームのようなものがついている時などがあるそうです。このような場合は、get_queryメソッドを上書きする必要があります。

```python:views.py
class ReviewList(generic.ListView):
    model = ReviewByClass

    def get_queryset(self):
        queryset = super().get_queryset()
        if True:
            queryset = queryset.filter(stars=5)
            # queryset = queryset.filter(title__contains="A")
        
        return queryset

```

filterの使い方については、[こちら](https://codelab.website/django-queryset-filter/)とかが参考になるかも知れません。

# 参考

[Djangoビギナーズブック](https://www.amazon.co.jp/Django%E3%83%93%E3%82%AE%E3%83%8A%E3%83%BC%E3%82%BA%E3%83%96%E3%83%83%E3%82%AF-%E6%BB%9D%E6%BE%A4-%E6%88%90%E4%BA%BA/dp/4877834699): クラスベース汎用ビューの仕組みが詳しく書いてあります。