【Django】ListViewの使い方と出来ること

# はじめに
Djangoの標準ビューの一つでモデルを単純に利用した表示を行うListViewが用意されています。
1種類のモデルを利用した画面の作成に有効ですが、使い方に迷ったので、まとめてみます。

## 環境
- Python 3.8
- Django 3.1

## モデル
本記事では、画面表示するモデルとして下記を使用することにします。

```python:models.py
class Article(models.Model):
    title = models.CharField(max_length=128)
    content = models.TextField()
    is_published = models.BooleanField()
```

# 使用例

## シンプルに使う

ListViewを継承したビュークラスに対して、単純にモデルとテンプレート名だけ指定する状態です。
クラス変数の`models`に表示したいモデルの名前、`template_name`にレンダリングに使用するテンプレートファイルをすると、指定したモデルの全ての保存済みオブジェクトを取得して、テンプレートでレンダリングするようになります。
この時、テンプレート側にはモデルのオブジェクトは`object_list`という名前で渡されます。

```python:views.py
from django.views.generic import ListView
from .models import Article

class ArticleListView(Listview):
    template_name = 'article_list.html'
    model = Article
```

```html:article_list.html
<!DOCTYPE html>
<html>
<head></head>
<body>
  <!-- object_list は Article.objects.all() と同じ結果となる -->
  {% for article in object_list %}
  <li>{{ article.title }}</li>
  {% endfor %}
</body>
</html>
```

## テンプレートに渡すモデルを並び変える

`ordering`というクラス変数を利用して、モデルの並び替えができます。
モデル操作時にしようする`order_by`に渡す並び替えに使用するキーを指定すると、そのキーで並び替えられた状態でテンプレートに渡されるようになります。

```python:views.py
from django.views.generic import ListView
from .models import Article

class ArticleListView(Listview):
    template_name = 'article_list.html'
    model = Article
    ordering = '-title' # order_by('-title')
```

## モデルをフィルタ・絞り込みする
### QuerySetを指定する方法

`model`でモデルを指定する代わりに`query_set`にクエリを設定することができます。
これを利用すると、モデルのフィルタや絞り込み、並び替え等を行うことができます。

```python:views.py
from django.views.generic import ListView
from .models import Article

class ArticleListView(Listview):
    template_name = 'article_list.html'
    query_set = Article.objects.filter(is_published=True) # is_publish が Trueのものに絞る
```

### get_querysetメソッドを使う方法

`query_set`にQuerySetを設定する方法では、単純なワンライナー程度の設定しかできませんでしたが、`get_query_set`というメソッドをオーバーライドすることで、より自由度の高い実装が可能です。

リクエストのGETパラメータの内容によって動作を変えたり、非表示（論理削除）のモデルを除外する等の複雑な制御が可能です。

この関数は既存のメソッドを拡張する形となるので、最初にスーパークラス(ListView)の`get_query_set`を実行して元になるQuerySetを取得します。
それをもとに必要な操作を行って加工を行い、最終的なQuerySetを戻り値として返すようにします。
このとき実行するスーパークラスの`get_query_set`は、クラス変数の`model`や`ordering`を設定値を利用した結果の断面のQuerySetが返ってきます。

```python:views.py
from django.views.generic import ListView
from .models import Article

class ArticleListView(Listview):
    template_name = 'article_list.html'
    model = Article

    def get_queryset(self, **kwargs):
        queryset = super().get_queryset(**kwargs) # Article.objects.all() と同じ結果

        # GETリクエストパラメータにkeywordがあれば、それでフィルタする
        keyword = self.request.GET.get('keyword')
        if keyword is not None:
            queryset = queryset.filter(title__contains=keyword)

        # is_publishedがTrueのものに絞り、titleをキーに並び変える
        queryset = queryset.filter(is_published=True).order_by('-title')

        return queryset
```

## コンテキストを加工する

`get_context_data`メソッドをオーバーライドすることで、テンプレートに渡すコンテキストを加工することもできます。

このメソッドも既存のListViewが持つメソッドをオーバーライドする形となりますので、最初にスーパークラスの`get_context_data`を実行して元になるコンテキストデータを取得して加工します。
スーパークラスのメソッドは、`object_list`（表示対象モデルのリスト）等が格納された状態のコンテキストが返ってくるため、それに対し要素の追加や変更を行いメソッドの戻り値として返します。

ここで返す結果はViewで自前でコンテキストに変数を設定してレンダリングする場合と同じように、テンプレート内で使用することができます。

```python:views.py
from django.views.generic import ListView
from .models import Article

class ArticleListView(Listview):
    template_name = 'article_list.html'
    model = Article
    
    def get_context_data(self):
        ctx = super().get_context_data()
        
        # page_title を追加する
        ctx['page_title'] = 'hoge'
        
        return ctx
```

```html:article_list.html
<!DOCTYPE html>
<html>
<head></head>
<body>
  <!-- 追加した変数を使用可能 -->
  <h1>{{ page_title }}</h1>
  
  {% for article in object_list %}
  <li>{{ article.title }}</li>
  {% endfor %}
</body>
</html>
```

## テンプレートに渡すモデルのオブジェクト名を変える

テンプレートに渡されるオブジェクトは`object_list`という名前でしたが、クラス変数の`context_object_name`に名前を指定することで、任意の名前に変更することもできます。

```python:views.py
from django.views.generic import ListView
from .models import Article

class ArticleListView(Listview):
    template_name = 'article_list.html'
    model = Article
    context_object_name = 'articles' # オブジェクト名を articles に指定
```

```html:article_list.html
<!DOCTYPE html>
<html>
<body>
  {% for article in articles %} <!-- object_list から articles に変更 -->
  <li>{{ article.title }}</li>
  {% endfor %}
</body>
</html>
```