DjangoのListViewクラスについて理解する

## はじめに
ListViewはとても簡単にリスト表示やページネーション機能を実装することができます。
基本的に属性を指定すれば動くのですが、コードを理解しないと柔軟に対応できない恐れがあります。
本記事で理解いただければ幸いです。

ListViewの全体コードは[こちら](https://github.com/django/django/blob/3.1/django/views/generic/list.py)


## ListViewの処理の流れ
ListViewは```MultipleObjectTemplateResponseMixin```と```BaseListView```を継承しています。
継承順により、```MultipleObjectTemplateResponseMixin```が先に実行され、その後に```BaseListView```の処理が行われます。

```python
class ListView(MultipleObjectTemplateResponseMixin, BaseListView):
```

### 継承① MultipleObjectTemplateResponseMixin
このMixinはTemplateResponseMixinを継承し、テンプレートの指定やHTTPレスポンスの生成を行います。

### 継承② BaseListView
```MultipleObjectMixin```と```View```を継承しており、ここでリストの取得やページネーション、コンテキストデータの生成が行われます。
Viewクラスの```as_view()```メソッドにより、適切なHTTPメソッドへのマッピングが行われます。


## MultipleObjectMixinクラス
このクラスでは、リストの取得やページネーション、コンテキストデータの生成などが行われます。
 ```python
 class MultipleObjectMixin(ContextMixin):
    allow_empty = True
    queryset = None
    model = None
    paginate_by = None
    paginate_orphans = 0
    context_object_name = None
    paginator_class = Paginator
    page_kwarg = 'page'
    ordering = None

    def get_queryset(self):
        if self.queryset is not None:
            queryset = self.queryset
            if isinstance(queryset, QuerySet):
                queryset = queryset.all()
        elif self.model is not None:
            queryset = self.model._default_manager.all()
        else:
            raise ImproperlyConfigured(
                "%(cls)s is missing a QuerySet. Define "
                "%(cls)s.model, %(cls)s.queryset, or override "
                "%(cls)s.get_queryset()." % {
                    'cls': self.__class__.__name__
                }
            )
        ordering = self.get_ordering()
        if ordering:
            if isinstance(ordering, str):
                ordering = (ordering,)
            queryset = queryset.order_by(*ordering)

        return queryset

    def get_ordering(self):
        return self.ordering

    def paginate_queryset(self, queryset, page_size):
        paginator = self.get_paginator(
            queryset, page_size, orphans=self.get_paginate_orphans(),
            allow_empty_first_page=self.get_allow_empty())
        page_kwarg = self.page_kwarg
        page = self.kwargs.get(page_kwarg) or self.request.GET.get(page_kwarg) or 1
        try:
            page_number = int(page)
        except ValueError:
            if page == 'last':
                page_number = paginator.num_pages
            else:
                raise Http404(_('Page is not “last”, nor can it be converted to an int.'))
        try:
            page = paginator.page(page_number)
            return (paginator, page, page.object_list, page.has_other_pages())
        except InvalidPage as e:
            raise Http404(_('Invalid page (%(page_number)s): %(message)s') % {
                'page_number': page_number,
                'message': str(e)
            })

    def get_paginate_by(self, queryset):
        return self.paginate_by

    def get_paginator(self, queryset, per_page, orphans=0,
                      allow_empty_first_page=True, **kwargs):

        return self.paginator_class(
            queryset, per_page, orphans=orphans,
            allow_empty_first_page=allow_empty_first_page, **kwargs)

    def get_paginate_orphans(self):
        return self.paginate_orphans

    def get_allow_empty(self):
        return self.allow_empty

    def get_context_object_name(self, object_list):
        if self.context_object_name:
            return self.context_object_name
        elif hasattr(object_list, 'model'):
            return '%s_list' % object_list.model._meta.model_name
        else:
            return None

    def get_context_data(self, *, object_list=None, **kwargs):
        queryset = object_list if object_list is not None else self.object_list
        page_size = self.get_paginate_by(queryset)
        context_object_name = self.get_context_object_name(queryset)
        if page_size:
            paginator, page, queryset, is_paginated = self.paginate_queryset(queryset, page_size)
            context = {
                'paginator': paginator,
                'page_obj': page,
                'is_paginated': is_paginated,
                'object_list': queryset
            }
        else:
            context = {
                'paginator': None,
                'page_obj': None,
                'is_paginated': False,
                'object_list': queryset
            }
        if context_object_name is not None:
            context[context_object_name] = queryset
        context.update(kwargs)
        return super().get_context_data(**context)
 ```
### クラス変数
| 変数名 | 役割 |
| ---- | ---- |
| allow_empty | クエリセットが空の場合にエラーを出すかどうかを決める変数。デフォルトのTrueならエラーを発生させません |
| queryset | ビューが操作するクエリセットを定義します。設定されていない場合はmodel属性が使われます。 |
| model | querysetが設定されていない場合、このモデルからクエリセットが自動的に作成されます。 |
| paginate_by | 最後のページに表示するオブジェクトの最小数を指定します。この数未満の場合、それらは前のページに結合されます。 |
| context_object_name | テンプレートでクエリセットにアクセスするために使用されるコンテキスト変数の名前を指定します。設定されていない場合、object_listが使用されます。 |
| paginator_class | デフォルトはPaginatorクラスで、ページネーションに使用されるクラスを指定します。カスタムページネーターを使用する場合は、この属性をオーバーライドします。 |
| page_kwarg | URLからページ番号を取得するために使用されるキーワード引数の名前を指定します。 |
| ordering | クエリセットの並び順を指定します。この属性を設定することで、オブジェクトのデフォルトの並び順をオーバーライドできます。 |
 
 ### get_querysetメソッド
クエリセットを取得するためのメソッドで、データベースからのデータ取得、フィルタリング、並べ替えを行います。
クエリセットとはデータベースからのデータの取得やフィルタリング、並べ替えなどの操作を抽象的に行うためのものであり、SQLクエリに変換されるので、Pythonのコードとして直感的に操作できるのがクエリセットの大きなメリットです。
 ```Python:クエリセットの例
 all_objects = MyModel.objects.all()
filtered_objects = MyModel.objects.filter(name="John")
first_five = MyModel.objects.all()[:5]
 ```
コードでは初めに```queryset```の存在を確認し、それが```QuerySet```インスタンスである場合、```queryset.all()```を使って新しいクエリセットを複製します。
all()メソッドは新しいクエリセットを返し、元のクエリセットに影響を与えないため、ビュー外でqueryset属性が変更されることを防ぎ、ビュー内でのみクエリセットを操作するために行われます。

```queryset```は```QuerySet```のメソッド（.filter()、.all()など)を持っており、データベースへのクエリが「遅延評価」されることを意味します。
反対に、querysetがリストや他のイテラブルである場合はデータベースの遅延評価のメリットを享受できません。使い道としては特定のリスト操作やクエリセットからデータを即座に取得する必要がある場合に使用する必要があります。
ただし、一般的な使用ではQuerySetインスタンスを使用することを推奨しており。これによりデータベースの効率的な操作や、クエリの遅延評価、およびDjangoの多くの組み込み機能を利用することができます。
```Python:クエリセットとそうでない場合の例
# クエリセット
queryset = Article.objects.filter(published=True)
# リスト
queryset = list(Article.objects.filter(published=True))
```
querysetが存在しない場合は```model```が存在しているかを確認し、存在していれば```self.model._default_manager.all()```でモデルからすべてのオブジェクトを取得します。
いずれも存在しない場合はエラーを出力します。

リストをソートしたい場合は、orderingに値をセットすることで容易に実現することができます。

```python:ソート例
#昇順
class BookListView(ListView):
    model = Book
    ordering = 'title'

#降順
class BookListView(ListView):
    model = Book
    ordering = '-publish_date'

#動的にソートする
class BookListView(ListView):
   model = Book
    def get_ordering(self):
        user_ordering = self.request.GET.get('order')
        if user_ordering == 'desc':
            return '-title'
        return 'title'  
```
### paginate_querysetメソッド
クエリセットをページネーションするためのメソッドで、与えられたクエリセットをページネーションすることができます。

まずは```get_paginator```メソッドを使用して新しいPaginatorクラスのインスタンスを作成します。このインスタンスは、クエリセットのページネーションに使用されます。(Paginatorクラスについては[こちら](https://qiita.com/ryoutancobu/items/3d9bde2e8c6e7bc1f3aa))

取得したいページ番号は```page = self.kwargs.get(page_kwarg) or self.request.GET.get(page_kwarg) or 1```で取得しており、3つの異なるソースからページ番号を取得することができます。
```self.kwargs```はビュー関数に渡されるキーワード引数を含む辞書で、URLconfの```<int:page>```のようなURLのパターンマッチングから得られるキーワード引数を含んでいます。一つ目はそこからpage_kwarg(page)に一致するkeyの値(ページ番号を取得しています。)
```self.request.GET.get(page_kwarg)```は```/items/?page=2```のようなクエリパラメータからページ番号を取得します。
そして上記二つに当てはまらない場合は```1```を取得します。
デフォルトのpage_kwargは```page```という名前ですが、変更したい場合はビュー内で属性をオーバーライドすることができます。
```Python:page_kwargを変更する例
class MyListView(ListView):
    model = MyModel
    paginate_by = 10
    page_kwarg = 'mypage'
```
```Python
path('items/page/<int:mypage>/', MyListView.as_view(), name='item-list-page'),
```
次に取得したpage(ページ番号)を元に実際にデータを取得していきます。
int型にキャストして、もしエラーになれば```last```という文字列か確認します。lastの場合、ページ番号はページネータの最後のページとして（paginator.num_pages）として設定されます。```num_pages```は、Paginatorクラスのメソッドで、オブジェクトの総数をページあたりのオブジェクト数で割り、その結果を切り上げることで必要な総ページ数を求めることができます。なので、pageがlastの場合、```page_number = paginator.num_pages```によってpage_numberに必要なページ数が格納されることになります。

数値にキャストされるか必要なページ数を取得した後は、paginatorクラスの```page```メソッドで指定されたページ番号に対応するページオブジェクトを取得します。
そして最終的に```(paginator, page, page.object_list, page.has_other_pages())```を返します。
```paginator```はクエリセット全体をページネーションするためのオブジェクトです。
```python
paginator.count: #全オブジェクトの総数を取得します。
paginator.num_pages: #全ページ数を取得します。
paginator.page_range: #使用可能なページ番号のリストを取得します。
```
```page```は現在のページに関連する情報やオブジェクトのサブセットへのアクセスを提供します。
```python
page.number: #現在のページ番号を取得します。
page.has_previous(): #前のページが存在する場合はTrueを返します。
page.has_next(): #次のページが存在する場合はTrueを返します。
page.previous_page_number(): #前のページの番号を取得します。
page.next_page_number(): #次のページの番号を取得します。
```
```page.object_list```は現在のページに表示されるオブジェクトのサブセットで、ループさせることでページ上の各アイテムを操作または表示します
```python
for item in page.object_list:
    print(item)
```
```page.has_other_pages```はbool型で前または次に他のページが存在する場合はTrueを返し、存在しない場合はFalseを返します。

### get_context_dataメソッド
 テンプレートに渡されるコンテキストデータを取得するためのメソッドです。

基本的にはビュー内で get_querysetメソッドをオーバーライドすることでリストを操作しますが、例えば、あるビューの結果を基に、その結果を表示するための別のビューにリダイレクトする場合には引数の```object_list```を使うことになります。
```python
class MyCustomView(View):
    template_name = 'my_template.html'
    
    def get(self, request, *args, **kwargs):
        # 何らかの処理を行い、オブジェクトリストを生成
        object_list = MyModel.objects.filter(some_field='some_value')
        
        # オブジェクトリストと共に別のビューにリダイレクトする
        return render(request, self.template_name, self.get_context_data(object_list=object_list))
    
    def get_context_data(self, *, object_list=None, **kwargs):
        # object_listが提供されていない場合は、デフォルトのクエリセットを使用
        if object_list is None:
            object_list = MyModel.objects.all()
        
        # コンテキストデータを生成
        context = {
            'object_list': object_list,
            # 追加のコンテキストデータを設定
            'additional_data': 'これは追加のデータです'
        }
        
        return context
```
上記の場合はobject_listにオブジェクトリストが引数に指定されているので、```get_context_data```内で指定されたオブジェクトリストをcontextのobject_listキーに追加しています。

get_context_dataメソッドでは最初に```queryset = object_list if object_list is not None else self.object_list```でobject_listがあればそれを```queryset```にセットし、そうでなければデフォルトのクエリセットをセットします。

page_sizeはget_paginate_byから取得し、値があれば```paginate_queryset```メソッドの引数にpage_sizeを入れてリストデータをテンプレートで使用するcontextデータに格納します。
値がなければデフォルトのobject_listを返します。
つまり、ページネーションが指定されていれば(ページサイズ)区切り、そうでなければデフォルトのobject_list(queryset)をcontextに入れているということです。

最後にcontext_object_nameが存在すれば、querysetの名前を指定した名前にして、最終的なcontextを親スーパーのget_context_dataにcontextを渡します。

## BaseListViewクラス
```MultipleObjectMixin```と```View```クラスを継承し、```ListView```の基本機能を提供します。
カスタムレスポンスの生成やリスト表示以外の処理を組み込みたい場合にはこのクラスが使用されます。

以下は、BaseListViewを使用して独自のHTTPレスポンスを生成する例です。
```python
class BookListView(BaseListView):
    model = Book

    def get(self, request, *args, **kwargs):
        self.object_list = self.get_queryset()
        context = self.get_context_data()
        # contextを使って独自のHttpResponseを生成
        book_titles = ', '.join([book.title for book in context['object_list']])
        return HttpResponse(f'Books: {book_titles}')

    def get_context_data(self, **kwargs):
        # ベースクラスのコンテキストデータを取得
        context = super().get_context_data(**kwargs)
        # 必要に応じてコンテキストデータをカスタマイズ
        context['extra_data'] = 'This is extra data for the view.'
        return context
```
getメソッドで取得されたオブジェクトのリストを使用して独自のテキストベースのHTTPレスポンスを生成しています。ビューにアクセスすると、すべての書籍のタイトルがコンマ区切りで表示されるレスポンスが返されます。
独自のレンダリング方法や特定のHTTPレスポンスのフォーマットが必要な場合に有効ですが、HTMLテンプレートを使用して通常のWebページをレンダリングする場合は、ListViewのような派生クラスを使用する方が効率的です。

## MultipleObjectTemplateResponseMixinクラス
複数のオブジェクトをテンプレートに渡すために使用され、テンプレート名の生成などを行います。

```python:MultipleObjectTemplateResponseMixinクラス
class MultipleObjectTemplateResponseMixin(TemplateResponseMixin):
    """Mixin for responding with a template and list of objects."""
    template_name_suffix = '_list'

    def get_template_names(self):
        try:
            names = super().get_template_names()
        except ImproperlyConfigured:
            names = []

        if hasattr(self.object_list, 'model'):
            opts = self.object_list.model._meta
            names.append("%s/%s%s.html" % (opts.app_label, opts.model_name, self.template_name_suffix))
        elif not names:
            raise ImproperlyConfigured(
                "%(cls)s requires either a 'template_name' attribute "
                "or a get_queryset() method that returns a QuerySet." % {
                    'cls': self.__class__.__name__,
                }
            )
        return names

```
### get_template_namesメソッド
まずは継承した```TemplateResponseMixin```の```get_template_names```メソッドを使用してnamesを取得します。もしnamesが存在しなければ、空の配列をセットします。

object_listに```model```属性が存在していればそのモデルのメタデータオブジェクトを```opts```に格納し、```names```にoptsの```app_label```、```model_name```、template_name_suffixを使用してtemplate_nameを作成し返却します。

## ListViewクラス
上記のMixinとクラスを組み合わせ、リスト表示に関連する一連のタスクを容易にします。
例えば、モデルの指定やテンプレート名、ページネーションの設定などが可能です。

```python:使用例
class BookListView(ListView):
    model = Book  # 表示するモデルを指定
    template_name = 'book_list.html'  # 使用するテンプレートを指定
    context_object_name = 'books'  # テンプレートで使用するコンテキスト変数の名前を指定
    paginate_by = 10  # 1ページに表示するオブジェクトの数を指定
```

```html
<h1>Book List</h1>
<ul>
    {% for book in books %}
        <li>{{ book.title }} by {{ book.author }}</li>
    {% endfor %}
</ul>
```

以下のmodelとtemplate_nameを指定するだけで、テンプレートでobject_listを使用することができます。
```python
class IndexListView(ListView):
    model = Item
    template_name = 'pages/index.html'
```