Django 的 ListView 類視圖詳解
本小節(jié)將繼續(xù)介紹 Django 中常用的 ListView 類視圖并深入分析其實(shí)現(xiàn)原理,最后達(dá)到完全掌握該視圖類的目的。
1. ListView 類視圖介紹和使用
ListView 類從名字上看應(yīng)該是處理和列表相關(guān)的視圖,事實(shí)也是如此。我們同樣基于前面 TemplateView 中實(shí)現(xiàn)的例子,使用 ListView 來(lái)減少代碼,體驗(yàn)下 ListView 視圖類給我們帶來(lái)的便捷。
實(shí)驗(yàn)1:重現(xiàn) TemplateView 功能;
首先我們完成前面 TemplateView 的簡(jiǎn)單功能,然后在提出幾個(gè)報(bào)錯(cuò)的問題,這些問題比較簡(jiǎn)單,只要看下報(bào)錯(cuò)位置和源碼信息就非常清楚了。
首先我先給出一個(gè)基本的知識(shí):ListView 具備 TemplateView 所有的功能與屬性,并做了許多擴(kuò)展。那么前面由TemplateView 實(shí)現(xiàn)的所有示例直接將 TemplateView 替換成 ListView 也是可以運(yùn)行的?
我們以最簡(jiǎn)單的一個(gè)模板例子進(jìn)行演示:
在 hello_app/views.py
中新增一個(gè)視圖類 TestListView1:
(django-manual) [root@server first_django_app]# cat templates/test1.html
<p>{{ content }}</p>
<div>{{ spyinx.age }}</div>
class TestListView1(ListView):
template_name = 'test1.html'
在 hello_app/urls.py
中新增一個(gè) URLConf 配置:
urlpatterns = [
# ...
path('test_list_view1/', views.TestListView1.as_view(extra_context=context_data), name='test_list_view1')
]
使用 runserver
命令啟動(dòng)后,請(qǐng)求對(duì)應(yīng)的 URL 地址,發(fā)現(xiàn)異常,錯(cuò)誤原因也很明顯,缺少queryset。
上面的出錯(cuò)是在父類的 get() 方法中,那么修改 hello_app/views.py
位置的視圖類 TestListView1,重新定義自己的 get() 方法,如下:
class TestListView1(ListView):
template_name = 'test1.html'
def get(self, request, *args, **kwargs):
return self.render_to_response(context={'content': '正文1', 'spyinx': {'age': 29}})
啟動(dòng)服務(wù)后同樣報(bào)錯(cuò),不過這次錯(cuò)誤不一樣了,如下:
同樣顯示的是沒有對(duì)象列表。我們通過查看源碼也能輕易解決這個(gè)問題。這個(gè)問題留到后面分析原源碼的時(shí)候去解決?,F(xiàn)在直接給出兩個(gè)報(bào)錯(cuò)的解決方案,如下:
# 解決第一個(gè)沒有自定義get()函數(shù)報(bào)錯(cuò)
class TestListView1(ListView):
template_name = 'test1.html'
queryset = Member.objects.all()
# 另一種寫法也是可以的
# model = Member
# 解決第二個(gè)自定義get()函數(shù)報(bào)錯(cuò)
class TestListView1(ListView):
template_name = 'test1.html'
object_list = Member.objects.all()
def get(self, request, *args, **kwargs):
return self.render_to_response(context={'content': '正文1', 'spyinx': {'age': 29}})
最后正確的結(jié)果如下,這里直接用 curl
命令請(qǐng)求結(jié)果顯示即可。
[root@server ~]# curl http://127.0.0.01:8888/hello/test_list_view1/
<p>正文1</p>
<div>29</div>
實(shí)驗(yàn)2:簡(jiǎn)化分頁(yè)代碼。同樣前面 TemplateView 做的那個(gè)顯示會(huì)員列表的基礎(chǔ)上,簡(jiǎn)化原來(lái)的代碼。
準(zhǔn)備原來(lái)的模板文件,修改分頁(yè)那塊代碼:
(django-manual) [root@server first_django_app]# cat templates/test.html
<html>
<head>
<style type="text/css">
.page{
margin-top: 10px;
font-size: 14px;
}
.member-table {
width: 50%;
text-align: center;
}
</style>
</head>
<body>
<p>會(huì)員信息-第{{ page_obj.number }}頁(yè)/共{{ paginator.num_pages }}頁(yè), 每頁(yè){{ paginator.per_page }}條, 總共{{ paginator.count }}條</p>
<div>
<table border="1" class="member-table">
<thead>
<tr>
<th>姓名</th>
<th>年齡</th>
<th>性別</th>
<th>職業(yè)</th>
<th>所在城市</th>
</tr>
</thead>
<tbody>
{% for member in members %}
<tr>
<td>{{ member.name }}</td>
<td>{{ member.age }}</td>
{% if member.sex == 0 %}
<td>男</td>
{% else %}
<td>女</td>
{% endif %}
<td>{{ member.occupation }}</td>
<td>{{ member.city }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<div >
<div class="page">
</div>
</div>
</div>
</body>
</html>
添加一個(gè)新的 ListView 視圖類,如下:
class TestListView2(ListView):
template_name = 'test.html'
model = Member
queryset=Member.objects.all()
paginate_by = 10
ordering = ["-age"]
context_object_name = "members"
注意:ordering 是設(shè)置顯示列表的排序字段,字符串前面的 “-” 號(hào)表示的是按照這個(gè)字段倒序排列,可以設(shè)置多個(gè)排序字段。context_object_name 一定要設(shè)置,對(duì)應(yīng)模板文件中的列表數(shù)據(jù)名。
添加 URLConf 配置:
urlpatterns = [
# ...
path('test_list_view2/', views.TestListView2.as_view(), name='test_list_view2')
]
啟動(dòng) first_django_app 工程,從瀏覽器上直接訪問這個(gè) url,就能看到和前面差不多的結(jié)果了??梢詡魅?page 參數(shù)控制第幾頁(yè),但是頁(yè)大小在視圖中已經(jīng)固定,無(wú)法改變。
從這個(gè)簡(jiǎn)單的例子,我們可以看到,相比前面用 TemplateView 手工對(duì)數(shù)據(jù)進(jìn)行分頁(yè),這里的 ListView 內(nèi)部已經(jīng)給我們實(shí)現(xiàn)了這樣的功能。我們只需要簡(jiǎn)單的配置下,設(shè)置好相關(guān)屬性,就能夠?qū)崿F(xiàn)對(duì)表的分頁(yè)查詢,這樣能節(jié)省重復(fù)的代碼操作,讓項(xiàng)目看起來(lái)簡(jiǎn)潔優(yōu)雅。但是我們一定要了解背后實(shí)現(xiàn)的邏輯,能看得懂源碼,這樣每一步的報(bào)錯(cuò),我們都能在源碼中找到原因,并迅速解決問題。接下來(lái)就是對(duì) ListView 視圖類源碼的學(xué)習(xí)與分析。
2. ListView 類視圖深入分析
首先在 VScode 中整體看看 ListView 的源代碼,其源碼路徑為: djnago/views/generic/list.py
。來(lái)看看ListView 類的整體繼承關(guān)系:
在紅框中出現(xiàn)的對(duì)象我們是在 TemplateView 中已經(jīng)遇到過了。這里可以看到 ListView 繼承的比 TemplateView 要多且復(fù)雜。我們來(lái)一個(gè)個(gè)分析這些基礎(chǔ)的類。
2.1 MultipleObjectTemplateResponseMixin
首先來(lái)看 MultipleObjectTemplateResponseMixin 這個(gè)對(duì)象,它是一個(gè) Mixin。前面我們提到,一個(gè) Mixin 就是一個(gè)包含一個(gè)或多個(gè)功能片段的對(duì)象。這里的 Mixin 是用于響應(yīng)模板文件和展示列表數(shù)據(jù)的,它繼承至前面介紹到的 TemplateResponseMixin,在 TemplateResponseMixin 上做的擴(kuò)展就是重寫了 get_template_names() 方法,其源碼如下:
class MultipleObjectTemplateResponseMixin(TemplateResponseMixin):
"""Mixin for responding with a template and list of objects."""
template_name_suffix = '_list'
def get_template_names(self):
"""
Return a list of template names to be used for the request. Must return
a list. May not be called if render_to_response is overridden.
"""
try:
names = super().get_template_names()
except ImproperlyConfigured:
# If template_name isn't specified, it's not a problem --
# we just start with an empty list.
names = []
# If the list is a queryset, we'll invent a template name based on the
# app and model name. This name gets put at the end of the template
# name list so that user-supplied names override the automatically-
# generated ones.
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
從這里的代碼,我們可以解釋第一個(gè)實(shí)驗(yàn)中,第二次添加 get() 方法后報(bào)錯(cuò)的原因,就在這個(gè)代碼段里。首先看這個(gè) get() 函數(shù):
def get(self, request, *args, **kwargs):
return self.render_to_response(context={'content': '正文1', 'spyinx': {'age': 29}})
這個(gè) get() 函數(shù)調(diào)用 self.render_to_response() 方法時(shí)會(huì)調(diào)用這個(gè) get_template_names() 方法。如果是在 TemplateView 中,直接這樣寫是毫無(wú)問題的,但是在 ListView 中,ListView 繼承了這個(gè) Mixin,然后調(diào)用的get_template_names() 方法正是這里的代碼。這個(gè) get_template_names() 方法相比原來(lái)的就是多了下半部分代碼,在程序執(zhí)行到下面的語(yǔ)句時(shí),由于沒有 object_list 屬性值就會(huì)觸發(fā)異常:
if hasattr(self.object_list, 'model'):
修正的方法很簡(jiǎn)單,只要一開始加上這個(gè) object_list 屬性值即可。對(duì)于這個(gè)object_list 屬性,它其實(shí)從名字也能看出來(lái),表示一個(gè)對(duì)象的列表值,其實(shí)是一個(gè) QuerySet 結(jié)果集。大概知道這些之后,我們就能理解后面的代碼了:
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__,
}
)
對(duì)于這段代碼指的是,如果self.object_list
對(duì)應(yīng)著一個(gè)模型時(shí),代碼會(huì)在 names 中添加一個(gè)默認(rèn)的模板文件名,我們可以在 shell 模式下理解下這些代碼:
(django-manual) [root@server first_django_app]# python manage.py shell
Python 3.8.1 (default, Dec 24 2019, 17:04:00)
[GCC 4.8.5 20150623 (Red Hat 4.8.5-39)] on linux
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> from hello_app.models import Member
>>> object_list = Member.objects.all()
>>> object_list.model
<class 'hello_app.models.Member'>
>>> object_list.model._meta
<Options for Member>
>>> opts = object_list.model._meta
>>> opts.app_label
'hello_app'
>>> opts.model_name
'member'
這就很明顯了,最后 names 中會(huì)加上一個(gè)額外的元素:hello_app/member_list.html
。現(xiàn)在我們可以立馬做一個(gè)實(shí)驗(yàn),將實(shí)驗(yàn)1中的 template_name 屬性值去掉,然后將原來(lái)的 test1.html 拷貝一份放到 template/hello_app
目錄下,操作如下:
(django-manual) [root@server first_django_app]# mkdir templates/hello_app
(django-manual) [root@server first_django_app]# cp templates/test1.html templates/hello_app/member_list.html
class TestListView1(ListView):
# template_name = 'test1.html'
model = Member
啟動(dòng)服務(wù),然后運(yùn)行發(fā)現(xiàn)也能成功。這就算對(duì)這個(gè) Mixin 掌握了,我們也理解了它的代碼內(nèi)容并獨(dú)立根據(jù)這個(gè)代碼內(nèi)容完成了一個(gè)實(shí)驗(yàn)。
(django-manual) [root@server first_django_app]# curl http://127.0.0.1:8888/hello/test_list_view1/
<p>正文1</p>
<div>29</div>
2.2 MultipleObjectMixin
這個(gè) Mixin 是用來(lái)幫助視圖處理多個(gè)對(duì)象的,如列表展示,分頁(yè)查詢都是在這里。這也是 ListView 視圖類的核心所在。來(lái)看看源代碼里面關(guān)于這個(gè) Mixin 的屬性和方法:
屬性:
-
allow_empty: 是否允許對(duì)象列表為空,默認(rèn)為 True;
-
queryset:對(duì)象的查詢集;
-
model:關(guān)聯(lián)的模型;
-
paginate_by: 分頁(yè)大??;
-
paginate_orphans: 這個(gè)比較有意思,需要通過代碼來(lái)詳細(xì)解釋下其含義。
# 源碼位置 django/core/paginator.py class Paginator: def __init__(self, object_list, per_page, orphans=0, allow_empty_first_page=True): self.object_list = object_list self._check_object_list_is_ordered() self.per_page = int(per_page) self.orphans = int(orphans) self.allow_empty_first_page = allow_empty_first_page # ... def page(self, number): """Return a Page object for the given 1-based page number.""" number = self.validate_number(number) bottom = (number - 1) * self.per_page top = bottom + self.per_page #這里可以看出 self.orphans 的含義 if top + self.orphans >= self.count: top = self.count return self._get_page(self.object_list[bottom:top], number, self) # ...
上面的
page()
方法是根據(jù)傳入的 number 獲取第幾頁(yè)數(shù)據(jù),每頁(yè)的大小由per_page
屬性確定。我們?cè)谇懊娴?TemplateView 中的分頁(yè)實(shí)例中知道,想要獲取第幾頁(yè)的數(shù)據(jù),可以按照如下公式:# 起始位置,number從1開始 start = (number - 1) * per_page # 結(jié)束位置,不包括end end = number * per_page # 另一種簡(jiǎn)單寫法 end = start + per_page # 數(shù)據(jù)切片,取第number頁(yè)數(shù)據(jù) object_list[start:end]
orphans
屬性的含義就體現(xiàn)在下面兩行代碼中:if top + self.orphans >= self.count: top = self.count
這里的含義是,如果計(jì)算出的下一頁(yè)的位置加上這個(gè)
orphans
屬性的值大于等于對(duì)象的總數(shù),也就是說(shuō)下一頁(yè)的數(shù)據(jù)如果少于orphans
的值,那么當(dāng)前這一頁(yè)需要把下一頁(yè)剩余的元素都選中。舉個(gè)例子,假設(shè)與102個(gè)數(shù)據(jù),現(xiàn)在按照每頁(yè)展示10條數(shù)據(jù),當(dāng)我展示到第10頁(yè)是,元素的位置應(yīng)該是 90-99,作為切片的話,應(yīng)該是[90:100],即bottom=90, top=100
。假設(shè)我設(shè)置orphans=3
,那么有100 + 3 > 102
,即最后一頁(yè)數(shù)目少于3個(gè),因此通過上面的邏輯判斷后,top=102
,此時(shí)顯示的列表切片為 [90:102]。 -
context_object_name:這個(gè)設(shè)置上下文中對(duì)象列表名稱。我們來(lái)翻看源代碼,查看這個(gè)屬性的含義,如下。
# 源碼位置:django/views/generic/list.py class MultipleObjectMixin(ContextMixin): # ... def get_context_object_name(self, object_list): """Get the name of the item to be used in the context.""" 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): """Get the context for this view.""" 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) # ...
查看源碼可以知道,這個(gè)屬性不設(shè)置也是有默認(rèn)值的(注意:只有在 object_list 沒設(shè)置,或者不是 QuerySet 時(shí),才返回 None 值)。通過
get_context_data()
代碼中的這樣一條語(yǔ)句:context[context_object_name] = queryset
這樣,在模板文件中,我們就可以使用 context_object_name 變量來(lái)循環(huán)顯示我們的對(duì)象列表了。
-
paginator_class:用于分頁(yè)的類,這種寫法讓 django 的分頁(yè)變得可擴(kuò)展,我們可以提供這樣的分頁(yè)類來(lái)替換掉 Django 中原有的分頁(yè)機(jī)制,從而實(shí)現(xiàn)我們自己的分頁(yè)控制。這種做法在可擴(kuò)展的模式中用的非常多,不過需要仔細(xì)研讀分頁(yè)的源碼,需要定義的屬性和方法才能替換官方的分頁(yè)類。
-
page_kwarg: 查詢頁(yè)號(hào)的 key 值。這個(gè)是指,查詢的頁(yè)號(hào)是從獲取參數(shù)的這個(gè) key 值中取出來(lái)的,可以是在 URLConf 配置中設(shè)定,也可以通過 GET 請(qǐng)求帶參數(shù)傳遞過來(lái)。來(lái)看看源碼里面如何使用這個(gè)屬性的,具體如下。
# 源碼路徑:django/views/generic/list.py class MultipleObjectMixin(ContextMixin): # ... def paginate_queryset(self, queryset, page_size): # ... page_kwarg = self.page_kwarg page = self.kwargs.get(page_kwarg) or self.request.GET.get(page_kwarg) or 1 # ... # ...
-
ordering:這個(gè)屬性是用來(lái)設(shè)置查詢列表的排序,可以放入多個(gè)排序字段。比如
ordering = ['name']
,表示結(jié)果集按照 name 字段從小到大排序,如果想按照倒序的順序,直接用ordering = ['-name']
即可。
方法:
get_queryset():返回視圖的對(duì)象列表:
class MultipleObjectMixin(ContextMixin):
# ...
def get_queryset(self):
"""
Return the list of items for this view.
The return value must be an iterable and may be an instance of
`QuerySet` in which case `QuerySet` specific behavior will be enabled.
"""
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
在這里,代碼邏輯也是非常清楚的。首先是需要理解 Django 的 ORM 操作。在一開始設(shè)置了 queryset 屬性時(shí),如果直接是 QuerySet 的實(shí)例,則會(huì)將用 .all()
將所有的數(shù)據(jù)取出來(lái),得到 queryset 并返回。當(dāng)然這里看代碼,不設(shè)置 queryset 也是可以的,設(shè)置好關(guān)聯(lián)的模型屬性 model,然后通過模型的默認(rèn) manager 調(diào)用 .all()
方法也能實(shí)現(xiàn)同樣的目標(biāo)。最后,在這里還使用了 ordering
的屬性值,如果有設(shè)置,則直接用 QuerySet 的 .ordering()
方法。
幾個(gè)簡(jiǎn)單的獲取屬性值得方法,如下:
- get_ordering():獲取排序字段;
- get_paginate_by(): 獲取分頁(yè)大??;
- get_paginator():實(shí)例化分頁(yè)對(duì)象,關(guān)聯(lián) paginator_class 屬性值;
- get_paginate_orphans(): 獲取 paginate_orphans 這個(gè)屬性值;
- get_allow_empty():獲取 allow_empty 這個(gè)屬性值;
- get_context_object_name():處理 context_object_name 這個(gè)屬性值;
class MultipleObjectMixin(ContextMixin):
# ...
def get_ordering(self):
"""Return the field or fields to use for ordering the queryset."""
return self.ordering
# ...
def get_paginate_by(self, queryset):
"""
Get the number of items to paginate by, or ``None`` for no pagination.
"""
return self.paginate_by
def get_paginator(self, queryset, per_page, orphans=0,
allow_empty_first_page=True, **kwargs):
"""Return an instance of the paginator for this view."""
return self.paginator_class(
queryset, per_page, orphans=orphans,
allow_empty_first_page=allow_empty_first_page, **kwargs)
def get_paginate_orphans(self):
"""
Return the maximum number of orphans extend the last page by when
paginating.
"""
return self.paginate_orphans
def get_allow_empty(self):
"""
Return ``True`` if the view should display empty lists and ``False``
if a 404 should be raised instead.
"""
return self.allow_empty
def get_context_object_name(self, object_list):
"""Get the name of the item to be used in the context."""
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
# ...
-
paginate_queryset():獲取分頁(yè)數(shù)據(jù)以及分頁(yè)信息。該函數(shù)會(huì)被 get_context_data() 方法調(diào)用生成上下文數(shù)據(jù),用于填充模板中的變量?jī)?nèi)容。該部分源碼會(huì)結(jié)合 get_context_data() 方法一起在下一小節(jié)中詳細(xì)介紹到;
-
get_context_data():獲取渲染模板的上下文數(shù)據(jù),也即分頁(yè)列表元素、分頁(yè)信息等,在下一部分內(nèi)容會(huì)詳細(xì)介紹該函數(shù)中的內(nèi)容。
2.3 BaseListView
講完上面的 MultipleObjectMixin 對(duì)象,ListView 視圖的基本功能其實(shí)就分析完了。 BaseListView 類繼承了 View 和 MultipleObjectMixin,并多添加了一個(gè) get() 方法。這也是 ListView 能直接處理 get 請(qǐng)求的原因。實(shí)驗(yàn)1中的第一個(gè)報(bào)錯(cuò)也是源自這里:self.object_list = self.get_queryset()
。只有定義了 queryset
或者 model
屬性時(shí),才能正常執(zhí)行下去。
class BaseListView(MultipleObjectMixin, View):
"""A base view for displaying a list of objects."""
def get(self, request, *args, **kwargs):
# 獲取對(duì)象列表
self.object_list = self.get_queryset()
# 是否設(shè)置允許為空
allow_empty = self.get_allow_empty()
if not allow_empty:
# 下面的if用于判斷數(shù)據(jù)是否為空,然后相應(yīng)設(shè)置is_empty值
if self.get_paginate_by(self.object_list) is not None and hasattr(self.object_list, 'exists'):
is_empty = not self.object_list.exists()
else:
is_empty = not self.object_list
# 在不許為空的條件中,如果為空直接拋出404異常
if is_empty:
raise Http404(_("Empty list and '%(class_name)s.allow_empty' is False.") % {
'class_name': self.__class__.__name__,
})
# 獲取分頁(yè)相關(guān)的數(shù)據(jù)
context = self.get_context_data()
# 渲染模板并返回
return self.render_to_response(context)
可以看到,這段代碼執(zhí)行的過程非常簡(jiǎn)單,很容易能看懂,我已經(jīng)在上面做好了簡(jiǎn)單的注釋。這段代碼中最重要的部分就在這一句中: context = self.get_context_data()
。這段代碼是要獲取相應(yīng)的分頁(yè)數(shù)據(jù)結(jié)果,然后調(diào)用 self.render_to_response(context)
來(lái)返回經(jīng)過渲染的模板文件,最后就是我們看到的那個(gè)會(huì)員列表頁(yè)面。self.get_context_data()
方法就是上面的 Mixin 提供的,函數(shù)源碼如下:
def get_context_data(self, *, object_list=None, **kwargs):
"""Get the context for this view."""
# 獲取對(duì)象列表
queryset = object_list if object_list is not None else self.object_list
# 獲取分頁(yè)大小
page_size = self.get_paginate_by(queryset)
# 獲取context_object_name,我們實(shí)驗(yàn)2中設(shè)置的就是members,對(duì)應(yīng)著模板中的變量
context_object_name = self.get_context_object_name(queryset)
if page_size:
# 核心的處理就是這一句,根據(jù)指定的分頁(yè)大小對(duì)數(shù)據(jù)集進(jìn)行分析,返回分頁(yè)的對(duì)象列表,分頁(yè)信息、是否分頁(yè)等
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:
# 沒有設(shè)置分頁(yè)大小,就是獲取全部數(shù)據(jù),不進(jìn)行分頁(yè)
context = {
'paginator': None,
'page_obj': None,
'is_paginated': False,
'object_list': queryset
}
# 設(shè)置模板中對(duì)象列表變量的數(shù)據(jù)
if context_object_name is not None:
context[context_object_name] = queryset
# context中再添加額外傳入的數(shù)據(jù)
context.update(kwargs)
# 最后調(diào)用父類的get_context_data()方法并返回
return super().get_context_data(**context)
上面的獲取上下文數(shù)據(jù)的代碼也比較簡(jiǎn)單,有分頁(yè)大小就調(diào)用self.paginate_queryset()
方法查詢分頁(yè)數(shù),沒有分頁(yè)大小就使用全部對(duì)象列表,然后構(gòu)造 context 值,最后調(diào)用父類的 get_context_data()
方法并返回??梢钥吹剑麄€(gè)獲取上下文數(shù)據(jù)的最核心處理就是 self.paginate_queryset()
這個(gè)方法了。它也是由上面介紹的那個(gè) Mixin 提供的,代碼如下:
def paginate_queryset(self, queryset, page_size):
"""Paginate the queryset, if needed."""
# 核心處理就是這一句
paginator = self.get_paginator(
queryset, page_size, orphans=self.get_paginate_orphans(),
allow_empty_first_page=self.get_allow_empty())
# 查詢第幾頁(yè)的key值,默認(rèn)是"page"
page_kwarg = self.page_kwarg
# 第幾頁(yè)的值會(huì)從kwargs或者GET請(qǐng)求中獲取,對(duì)應(yīng)的key就是上面的page_kwarg,沒有就默認(rèn)為1
page = self.kwargs.get(page_kwarg) or self.request.GET.get(page_kwarg) or 1
try:
# 會(huì)強(qiáng)制轉(zhuǎn)成int
page_number = int(page)
except ValueError:
# 當(dāng)強(qiáng)制轉(zhuǎn)換異常時(shí),處理邏輯也很清晰
if page == 'last':
page_number = paginator.num_pages
else:
raise Http404(_("Page is not 'last', nor can it be converted to an int."))
try:
# 生成對(duì)應(yīng)的page實(shí)例。一切正常時(shí),返回我們所需要的數(shù)據(jù),否則拋出異常
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)
})
當(dāng)這部分代碼能看懂時(shí),前面實(shí)驗(yàn)2部分的整個(gè)內(nèi)部邏輯,你差不多也就弄清楚了。雖然我們看著只需要配置幾個(gè)屬性,但是在 Django 內(nèi)部是替我們做了許多工作的,如果這部分工作并不是你想要的,這時(shí)候,就需要依據(jù)你自己的業(yè)務(wù)邏輯重寫相應(yīng)的函數(shù)了。如果能掌握整個(gè) ListView 視圖的執(zhí)行流程,在繼承它的時(shí)候就會(huì)感到胸有成竹,有錯(cuò)了就去根據(jù)錯(cuò)誤提示追蹤下源碼,這樣就不會(huì)再碰到錯(cuò)誤時(shí),不知道從何下手。所以,閱讀源碼是在學(xué)習(xí) Django 這樣的 Web 框架時(shí),非常重要的一個(gè)技能,而且很多關(guān)于 Django 的功能和用法我們都可以通過源碼來(lái)獲取。
最后,我們來(lái)看看 ListView 的代碼,其實(shí)就是單純繼承前面那個(gè)處理多個(gè)對(duì)象的 Mixin 和這個(gè) BaseListView:
class ListView(MultipleObjectTemplateResponseMixin, BaseListView):
"""
Render some list of objects, set by `self.model` or `self.queryset`.
`self.queryset` can actually be any iterable of items, not just a queryset.
"""
3. 小結(jié)
本節(jié)中,我們使用 ListView 完成了兩個(gè)小實(shí)驗(yàn),對(duì) ListView 有了一個(gè)基本的了解。接下來(lái),我們深入學(xué)習(xí)了和 ListView 相關(guān)的類和 mixin,并在源碼學(xué)習(xí)中完成了幾個(gè)簡(jiǎn)單的實(shí)驗(yàn)。在完成本節(jié)學(xué)習(xí)后,是不是對(duì) ListView 有了全新的了解?之后使用 ListView 報(bào)錯(cuò)后,是不是能迅速找到問題所在?如果是的話,那么本節(jié)也算起了一點(diǎn)小小的作用,作為本文作者,我也將感到無(wú)比榮耀。