Django初學(xué)者入門指南1-初識(shí)(譯&改)
Django初學(xué)者入門指南2-基礎(chǔ)知識(shí)(譯&改)
Django初學(xué)者入門指南3-高級(jí)概念(譯&改)
Django初學(xué)者入門指南4-登錄認(rèn)證(譯&改)
Django初學(xué)者入門指南5-存儲(chǔ)數(shù)據(jù)(譯&改)
Django初學(xué)者入門指南6-基于類的頁(yè)面(譯&改)
Django初學(xué)者入門指南7-部署發(fā)布(譯&改)
簡(jiǎn)介
歡迎來(lái)到教程系列的第5部分!在本教程中瘸彤,我們將學(xué)習(xí)更多用戶權(quán)限控制的知識(shí)奋早,還將實(shí)現(xiàn)主題、帖子列表視圖和回復(fù)頁(yè)面瘫里。還有凌简,我們將探討Django-ORM的一些特性肿轨,并簡(jiǎn)要介紹遷移寿冕。
限制訪問(wèn)頁(yè)面
我們必須限制未登錄用戶的頁(yè)面訪問(wèn)權(quán)限,比如下面這個(gè)創(chuàng)建新主題的頁(yè)面:
從導(dǎo)航條可以看出用戶沒(méi)有登錄椒袍,但是他依然能訪問(wèn)創(chuàng)建新主題的頁(yè)面:
DJango有一個(gè)內(nèi)置頁(yè)面裝飾器來(lái)解決這個(gè)問(wèn)題:
-
boards/views.py
(完整文檔地址)
from django.contrib.auth.decorators import login_required
@login_required
def new_topic(request, pk):
# ...
好了驼唱,現(xiàn)在用戶打開這個(gè)鏈接就會(huì)被重定向到登錄頁(yè)面:
注意這個(gè)URL請(qǐng)求的參數(shù)?next=/boards/1/new/,這里我們可以通過(guò)改進(jìn)登錄頁(yè)面響應(yīng)next參數(shù)來(lái)改進(jìn)用戶體驗(yàn)驹暑。
配置登錄頁(yè)面的重定向
templates/login.html (完整文檔地址)
<form method="post" novalidate>
{% csrf_token %}
<input type="hidden" name="next" value="{{ next }}">
{% include 'includes/form.html' %}
<button type="submit" class="btn btn-primary btn-block">Log in</button>
</form>
現(xiàn)在如果再進(jìn)行登錄玫恳,登錄成功后就會(huì)回到我們彈出登錄時(shí)我們想去的頁(yè)面了。
next這個(gè)參數(shù)是登錄頁(yè)面的一個(gè)內(nèi)置功能岗钩。
訪問(wèn)權(quán)限測(cè)試
現(xiàn)在讓我們寫一下@login_required
裝飾器是否正常工作纽窟。在此之前我們先重構(gòu)一下boards/tests/test_views.py
文件。
讓我們把test_views.py
拆成3個(gè)文件:
-
test_view_home.py
拷貝的是HomeTests測(cè)試類的內(nèi)容(完整文檔地址) -
test_view_board_topics.py
拷貝的是BoardTopicsTests測(cè)試類的內(nèi)容(完整文檔地址) -
test_view_new_topic.py
拷貝的是NewTopicTests測(cè)試類的內(nèi)容(完整文檔地址)
myproject/
|-- myproject/
| |-- accounts/
| |-- boards/
| | |-- migrations/
| | |-- templatetags/
| | |-- tests/
| | | |-- __init__.py
| | | |-- test_templatetags.py
| | | |-- test_view_home.py <-- 它
| | | |-- test_view_board_topics.py <-- 它
| | | +-- test_view_new_topic.py <-- 和它
| | |-- __init__.py
| | |-- admin.py
| | |-- apps.py
| | |-- models.py
| | +-- views.py
| |-- myproject/
| |-- static/
| |-- templates/
| |-- db.sqlite3
| +-- manage.py
+-- venv/
嘗試運(yùn)行一下測(cè)試用例看一切是否都正常兼吓。
我們?cè)賱?chuàng)建一個(gè)新的測(cè)試文件test_view_new_topic.py
來(lái)測(cè)試@login_required
裝飾器是否正常工作:
boards/tests/test_view_new_topic.py
(完整文檔地址)
from django.test import TestCase
from django.urls import reverse
from ..models import Board
class LoginRequiredNewTopicTests(TestCase):
def setUp(self):
Board.objects.create(name='Django', description='Django board.')
self.url = reverse('new_topic', kwargs={'pk': 1})
self.response = self.client.get(self.url)
def test_redirection(self):
login_url = reverse('login')
self.assertRedirects(self.response, '{login_url}?next={url}'.format(login_url=login_url, url=self.url))
上面的測(cè)試用例里我們嘗試在未登錄的情況下去訪問(wèn)創(chuàng)建新主題頁(yè)面new topic臂港,期望的結(jié)果是被重定位到登錄頁(yè)面。
已授權(quán)用戶的頁(yè)面訪問(wèn)
現(xiàn)在讓我們來(lái)改進(jìn)一下new_topic
视搏,由已登錄用戶進(jìn)行操作而不是從數(shù)據(jù)庫(kù)中暴力獲取第一個(gè)用戶审孽。之前是臨時(shí)的調(diào)試方法,現(xiàn)在我們有辦法進(jìn)行權(quán)限控制了浑娜,那么就修正它吧:
boards/views.py (完整文檔地址)
from django.contrib.auth.decorators import login_required
from django.shortcuts import get_object_or_404, redirect, render
from .forms import NewTopicForm
from .models import Board, Post
@login_required
def new_topic(request, pk):
board = get_object_or_404(Board, pk=pk)
if request.method == 'POST':
form = NewTopicForm(request.POST)
if form.is_valid():
topic = form.save(commit=False)
topic.board = board
topic.starter = request.user # <- here
topic.save()
Post.objects.create(
message=form.cleaned_data.get('message'),
topic=topic,
created_by=request.user # <- and here
)
return redirect('board_topics', pk=board.pk) # TODO: redirect to the created topic page
else:
form = NewTopicForm()
return render(request, 'new_topic.html', {'board': board, 'form': form})
我們可以添加一個(gè)新主題來(lái)測(cè)試一下:
主題的帖子列表
現(xiàn)在我們根據(jù)下面的線框圖來(lái)實(shí)現(xiàn)這個(gè)頁(yè)面吧:
要想富佑力,先修路:
myproject/urls.py (完整文檔地址)
<details>
<summary>原始版本</summary>
url(r'^boards/(?P<pk>\d+)/topics/(?P<topic_pk>\d+)/$', views.topic_posts, name='topic_posts'),
</details>
<details open>
<summary>修訂版本</summary>
re_path(r'^boards/(?P<pk>\d+)/topics/(?P<topic_pk>\d+)/$', views.topic_posts, name='topic_posts'),
</details>
注意這里我們定義了兩個(gè)參數(shù): pk
用來(lái)查詢版塊,然后還有一個(gè) topic_pk
用來(lái)查詢主題筋遭。
它的頁(yè)面方法就會(huì)是這樣的:
-
boards/views.py
(完整文檔地址)
from django.shortcuts import get_object_or_404, render
from .models import Topic
def topic_posts(request, pk, topic_pk):
topic = get_object_or_404(Topic, board__pk=pk, pk=topic_pk)
return render(request, 'topic_posts.html', {'topic': topic})
注意這里我們沒(méi)有直接獲取Board實(shí)例打颤,因?yàn)橹黝}Topic與版塊Board相關(guān),所以我們可以間接訪問(wèn)到當(dāng)前版塊的信息漓滔,你可以在下面的代碼里看到:
-
templates/topic_posts.html
(完整文檔地址)
{% extends 'base.html' %}
{% block title %}{{ topic.subject }}{% endblock %}
{% block breadcrumb %}
<li class="breadcrumb-item"><a href="{% url 'home' %}">Boards</a></li>
<li class="breadcrumb-item"><a href="{% url 'board_topics' topic.board.pk %}">{{ topic.board.name }}</a></li>
<li class="breadcrumb-item active">{{ topic.subject }}</li>
{% endblock %}
{% block content %}
{% endblock %}
這里可以看到编饺,我們用topic.board.name
來(lái)獲取版塊的名稱,而不是通過(guò)board.name
獲取响驴。
讓我們?cè)贋?code>topic_posts頁(yè)面寫一點(diǎn)測(cè)試用例吧:
boards/tests/test_view_topic_posts.py
from django.contrib.auth.models import User
from django.test import TestCase
from django.urls import resolve, reverse
from ..models import Board, Post, Topic
from ..views import topic_posts
class TopicPostsTests(TestCase):
def setUp(self):
board = Board.objects.create(name='Django', description='Django board.')
user = User.objects.create_user(username='john', email='john@doe.com', password='123')
topic = Topic.objects.create(subject='Hello, world', board=board, starter=user)
Post.objects.create(message='Lorem ipsum dolor sit amet', topic=topic, created_by=user)
url = reverse('topic_posts', kwargs={'pk': board.pk, 'topic_pk': topic.pk})
self.response = self.client.get(url)
def test_status_code(self):
self.assertEquals(self.response.status_code, 200)
def test_view_function(self):
view = resolve('/boards/1/topics/1/')
self.assertEquals(view.func, topic_posts)
可以看到setUp
方法變得越來(lái)越復(fù)雜透且,我們可以利用混合機(jī)制或者抽象類來(lái)重用代碼』砝穑或者也可以使用第三方庫(kù)來(lái)初始化測(cè)試代碼秽誊,減少這些樣板代碼鲸沮。
同樣的,現(xiàn)在我們項(xiàng)目的測(cè)試用例也越來(lái)越多锅论,執(zhí)行起來(lái)也越來(lái)越久讼溺,這里可以通過(guò)指定應(yīng)用程序來(lái)進(jìn)行測(cè)試:
python manage.py test boards
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.......................
----------------------------------------------------------------------
Ran 23 tests in 1.246s
OK
Destroying test database for alias 'default'...
也可以指定某一個(gè)測(cè)試文件來(lái)進(jìn)行測(cè)試:
python manage.py test boards.tests.test_view_topic_posts
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
..
----------------------------------------------------------------------
Ran 2 tests in 0.129s
OK
Destroying test database for alias 'default'...
甚至指定某一個(gè)測(cè)試用例的方法進(jìn)行測(cè)試:
python manage.py test boards.tests.test_view_topic_posts.TopicPostsTests.test_status_code
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.
----------------------------------------------------------------------
Ran 1 test in 0.100s
OK
Destroying test database for alias 'default'...
方便吧~
言歸正傳。
在topic_posts.html
里棍厌,我們利用一個(gè)循環(huán)來(lái)枚舉這個(gè)主題的所有帖子:
templates/topic_posts.html
{% extends 'base.html' %}
{% load static %}
{% block title %}{{ topic.subject }}{% endblock %}
{% block breadcrumb %}
<li class="breadcrumb-item"><a href="{% url 'home' %}">Boards</a></li>
<li class="breadcrumb-item"><a href="{% url 'board_topics' topic.board.pk %}">{{ topic.board.name }}</a></li>
<li class="breadcrumb-item active">{{ topic.subject }}</li>
{% endblock %}
{% block content %}
<div class="mb-4">
<a href="#" class="btn btn-primary" role="button">Reply</a>
</div>
{% for post in topic.posts.all %}
<div class="card mb-2">
<div class="card-body p-3">
<div class="row">
<div class="col-2">
<img src="{% static 'img/avatar.svg' %}" alt="{{ post.created_by.username }}" class="w-100">
<small>Posts: {{ post.created_by.posts.count }}</small>
</div>
<div class="col-10">
<div class="row mb-3">
<div class="col-6">
<strong class="text-muted">{{ post.created_by.username }}</strong>
</div>
<div class="col-6 text-right">
<small class="text-muted">{{ post.created_at }}</small>
</div>
</div>
{{ post.message }}
{% if post.created_by == user %}
<div class="mt-3">
<a href="#" class="btn btn-primary btn-sm" role="button">Edit</a>
</div>
{% endif %}
</div>
</div>
</div>
</div>
{% endfor %}
{% endblock %}
鑒于目前我們還沒(méi)有準(zhǔn)備上傳用戶頭像圖片的功能肾胯,先放一張站位圖吧竖席。
我下載的免費(fèi)圖片來(lái)自IconFinder耘纱,把它放到項(xiàng)目里的static/img目錄下。
現(xiàn)在我們還沒(méi)有涉及到DJango的關(guān)系對(duì)象映射(ORM)毕荐,實(shí)際上{{ post.created_by.posts.count }}
就是在數(shù)據(jù)庫(kù)中行類似select count
這樣的SQL束析,通過(guò)這樣的代碼雖然可以得到正確的結(jié)果,但是這實(shí)際上是一種非常糟糕的寫法憎亚,因?yàn)檫@里會(huì)對(duì)數(shù)據(jù)庫(kù)進(jìn)行多次沒(méi)必要的查詢操作员寇。讓我們先忽略它,當(dāng)下先集中精力到程序交互的實(shí)現(xiàn)上第美,稍后再對(duì)數(shù)據(jù)庫(kù)查詢這里的代碼進(jìn)行優(yōu)化蝶锋,解決重復(fù)查詢的問(wèn)題。
這里還有一個(gè)點(diǎn)事我們檢測(cè)這個(gè)帖子是否是當(dāng)前登錄的用戶發(fā)的:{% if post.created_by == user %}
什往,如果是的扳缕,那么就會(huì)在帖子上顯示出編輯按鈕。
再更新一下topics.html模板里的鏈接:
-
templates/topics.html
(完整文檔地址)
{% for topic in board.topics.all %}
<tr>
<td><a href="{% url 'topic_posts' board.pk topic.pk %}">{{ topic.subject }}</a></td>
<td>{{ topic.starter.username }}</td>
<td>0</td>
<td>0</td>
<td>{{ topic.last_updated }}</td>
</tr>
{% endfor %}
回復(fù)帖子頁(yè)面
讓我們實(shí)現(xiàn)回復(fù)帖子頁(yè)面别威,這樣就可以創(chuàng)建更多帖子來(lái)調(diào)試了躯舔。
配置新的URL路由:
-
myproject/urls.py
(完整文檔地址)
<details>
<summary>原始版本</summary>
url(r'^boards/(?P<pk>\d+)/topics/(?P<topic_pk>\d+)/reply/$', views.reply_topic, name='reply_topic'),
</details>
<details open>
<summary>修訂版本</summary>
re_path(r'^boards/(?P<pk>\d+)/topics/(?P<topic_pk>\d+)/reply/$', views.reply_topic, name='reply_topic'),
</details>
為回復(fù)頁(yè)面創(chuàng)建一個(gè)新的表單類:
-
boards/forms.py
(完整文檔地址)
from django import forms
from .models import Post
class PostForm(forms.ModelForm):
class Meta:
model = Post
fields = ['message', ]
也創(chuàng)建一個(gè)新的需要@login_required
裝飾器的頁(yè)面,再加上簡(jiǎn)單的表單處理邏輯:
-
boards/views.py
(完整文檔地址)
from django.contrib.auth.decorators import login_required
from django.shortcuts import get_object_or_404, redirect, render
from .forms import PostForm
from .models import Topic
@login_required
def reply_topic(request, pk, topic_pk):
topic = get_object_or_404(Topic, board__pk=pk, pk=topic_pk)
if request.method == 'POST':
form = PostForm(request.POST)
if form.is_valid():
post = form.save(commit=False)
post.topic = topic
post.created_by = request.user
post.save()
return redirect('topic_posts', pk=pk, topic_pk=topic_pk)
else:
form = PostForm()
return render(request, 'reply_topic.html', {'topic': topic, 'form': form})
別忘了在new_topic
頁(yè)面方法里標(biāo)記了# TODO的位置更新一下代碼省古。
@login_required
def new_topic(request, pk):
board = get_object_or_404(Board, pk=pk)
if request.method == 'POST':
form = NewTopicForm(request.POST)
if form.is_valid():
topic = form.save(commit=False)
# code suppressed ...
return redirect('topic_posts', pk=pk, topic_pk=topic.pk) # <- 這兒
# code suppressed ...
重點(diǎn): reply_topic頁(yè)面方法我們使用的參數(shù)是topic_pk
粥庄,引用的是方法關(guān)鍵字參數(shù);在new_topic頁(yè)面我們使用的是topic.pk
豺妓,引用的是Topic實(shí)例的屬性惜互。很小的細(xì)節(jié),但有很重要琳拭。
模板的第一個(gè)版本:
templates/reply_topic.html
{% extends 'base.html' %}
{% load static %}
{% block title %}Post a reply{% endblock %}
{% block breadcrumb %}
<li class="breadcrumb-item"><a href="{% url 'home' %}">Boards</a></li>
<li class="breadcrumb-item"><a href="{% url 'board_topics' topic.board.pk %}">{{ topic.board.name }}</a></li>
<li class="breadcrumb-item"><a href="{% url 'topic_posts' topic.board.pk topic.pk %}">{{ topic.subject }}</a></li>
<li class="breadcrumb-item active">Post a reply</li>
{% endblock %}
{% block content %}
<form method="post" class="mb-4">
{% csrf_token %}
{% include 'includes/form.html' %}
<button type="submit" class="btn btn-success">Post a reply</button>
</form>
{% for post in topic.posts.all %}
<div class="card mb-2">
<div class="card-body p-3">
<div class="row mb-3">
<div class="col-6">
<strong class="text-muted">{{ post.created_by.username }}</strong>
</div>
<div class="col-6 text-right">
<small class="text-muted">{{ post.created_at }}</small>
</div>
</div>
{{ post.message }}
</div>
</div>
{% endfor %}
{% endblock %}
用戶回復(fù)成功后训堆,會(huì)重定向到該主題下的帖子列表頁(yè)面:
讓我們?cè)俳o第一個(gè)帖子加一個(gè)效果進(jìn)行區(qū)分:
-
templates/topic_posts.html
(完整文檔地址)
{% for post in topic.posts.all %}
<div class="card mb-2 {% if forloop.first %}border-dark{% endif %}">
{% if forloop.first %}
<div class="card-header text-white bg-dark py-2 px-3">{{ topic.subject }}</div>
{% endif %}
<div class="card-body p-3">
<!-- code suppressed -->
</div>
</div>
{% endfor %}
現(xiàn)在再創(chuàng)建一些測(cè)試用例,就按我們之前的方式創(chuàng)建就可以了臀栈。創(chuàng)建一個(gè)新文件test_view_reply_topic.py
到boards/tests目錄下:
-
boards/tests/test_view_reply_topic.py
(完整文檔地址)
from django.contrib.auth.models import User
from django.test import TestCase
from django.urls import reverse
from ..models import Board, Post, Topic
from ..views import reply_topic
class ReplyTopicTestCase(TestCase):
'''
Base test case to be used in all `reply_topic` view tests
'''
def setUp(self):
self.board = Board.objects.create(name='Django', description='Django board.')
self.username = 'john'
self.password = '123'
user = User.objects.create_user(username=self.username, email='john@doe.com', password=self.password)
self.topic = Topic.objects.create(subject='Hello, world', board=self.board, starter=user)
Post.objects.create(message='Lorem ipsum dolor sit amet', topic=self.topic, created_by=user)
self.url = reverse('reply_topic', kwargs={'pk': self.board.pk, 'topic_pk': self.topic.pk})
class LoginRequiredReplyTopicTests(ReplyTopicTestCase):
# ...
class ReplyTopicTests(ReplyTopicTestCase):
# ...
class SuccessfulReplyTopicTests(ReplyTopicTestCase):
# ...
class InvalidReplyTopicTests(ReplyTopicTestCase):
# ...
這里我們主要是增加了ReplyTopicTestCase這個(gè)測(cè)試用例蔫慧,其他的測(cè)試用例預(yù)留在這里,后面再使用权薯。
我們先測(cè)試頁(yè)面的裝飾器@login_required
是否正常工作姑躲,然后檢測(cè)HTML的輸入睡扬、請(qǐng)求返回狀態(tài)碼,最后再測(cè)試一下提交有效和無(wú)效的表單黍析。
查詢數(shù)據(jù)集
現(xiàn)在讓我們花點(diǎn)時(shí)間來(lái)了解DJango模型類的API功能卖怜,首先改進(jìn)一下首頁(yè)頁(yè)面:
這里有三個(gè)點(diǎn)需要完成:
- 顯示版塊下的帖子總數(shù);
- 顯示版塊下的主題總數(shù)阐枣;
- 顯示最新的帖子的作者和發(fā)帖時(shí)間马靠。
編寫代碼前,讓我們先啟動(dòng)Python終端來(lái)進(jìn)行調(diào)試蔼两。
在我們開始用終端調(diào)試前甩鳄,先把所有的類的__str__
方法實(shí)現(xiàn)以下。
-
boards/models.py
(完整文檔地址)
from django.db import models
from django.utils.text import Truncator
class Board(models.Model):
# ...
def __str__(self):
return self.name
class Topic(models.Model):
# ...
def __str__(self):
return self.subject
class Post(models.Model):
# ...
def __str__(self):
truncated_message = Truncator(self.message)
return truncated_message.chars(30)
在帖子類里额划,我們使用Truncator工具類來(lái)截?cái)嗵幚硖酉?nèi)容妙啃,這樣可以很方便的將長(zhǎng)文本截?cái)酁槿我忾L(zhǎng)度。
讓我們打開Python終端:
python manage.py shell
# First get a board instance from the database
board = Board.objects.get(name='Django')
這三個(gè)點(diǎn)里最容易的就是獲取當(dāng)前主題的數(shù)量俊戳,因?yàn)榭梢詮?strong>Board關(guān)聯(lián)的Topic上直接獲取到:
board.topics.all()
<QuerySet [<Topic: Hello everyone!>, <Topic: Test>, <Topic: Testing a new post>, <Topic: Hi>]>
board.topics.count()
4
這樣就拿到了揖赴。
要獲取版塊下的帖子和回復(fù)總數(shù)就會(huì)稍微麻煩一點(diǎn),因?yàn)?strong>Board與Post沒(méi)有直接關(guān)系抑胎。
from boards.models import Post
Post.objects.all()
<QuerySet [<Post: This is my first topic.. :-)>, <Post: test.>, <Post: Hi everyone!>,
<Post: New test here!>, <Post: Testing the new reply feature!>, <Post: Lorem ipsum dolor sit amet,...>,
<Post: hi there>, <Post: test>, <Post: Testing..>, <Post: some reply>, <Post: Random random.>
]>
Post.objects.count()
11
我們總共有11個(gè)帖子燥滑,但是這里看不出來(lái)哪些是屬于DJango
版塊的。
需要通過(guò)下面的代碼篩選出我們需要的結(jié)果:
from boards.models import Board, Post
board = Board.objects.get(name='Django')
Post.objects.filter(topic__board=board)
<QuerySet [<Post: This is my first topic.. :-)>, <Post: test.>, <Post: hi there>,
<Post: Hi everyone!>, <Post: Lorem ipsum dolor sit amet,...>, <Post: New test here!>,
<Post: Testing the new reply feature!>
]>
Post.objects.filter(topic__board=board).count()
7
加了雙下劃線的字段topic__board
阿逃,對(duì)應(yīng)的就是Topic和Board的關(guān)系铭拧,這背后DJango給Board、Topic盆昙、Post建立了一個(gè)橋梁關(guān)系羽历,構(gòu)建了一個(gè)SQL查詢來(lái)檢索指定版塊的帖子。
最后我們需要獲取到最新的一個(gè)帖子實(shí)例淡喜。
# order by the `created_at` field, getting the most recent first
Post.objects.filter(topic__board=board).order_by('-created_at')
<QuerySet [<Post: testing>, <Post: new post>, <Post: hi there>, <Post: Lorem ipsum dolor sit amet,...>,
<Post: Testing the new reply feature!>, <Post: New test here!>, <Post: Hi everyone!>,
<Post: test.>, <Post: This is my first topic.. :-)>
]>
# we can use the `first()` method to just grab the result that interest us
Post.objects.filter(topic__board=board).order_by('-created_at').first()
<Post: testing>
搞定秕磷,我們現(xiàn)在可以開始寫代碼了。
-
boards/models.py
(完整文檔地址)
from django.db import models
class Board(models.Model):
name = models.CharField(max_length=30, unique=True)
description = models.CharField(max_length=100)
def __str__(self):
return self.name
def get_posts_count(self):
return Post.objects.filter(topic__board=self).count()
def get_last_post(self):
return Post.objects.filter(topic__board=self).order_by('-created_at').first()
注意觀察我們用了self
這個(gè)參數(shù)炼团,因?yàn)檫@個(gè)方法是由一個(gè)Board實(shí)例調(diào)用的澎嚣,所以這個(gè)參數(shù)的意思就是獲取自己這個(gè)Board實(shí)例,再用這個(gè)實(shí)例去過(guò)濾請(qǐng)求的數(shù)據(jù)集瘟芝。
改進(jìn)一下首頁(yè)的HTML模板易桃,顯示上面獲取到的信息
templates/home.html
{% extends 'base.html' %}
{% block breadcrumb %}
<li class="breadcrumb-item active">Boards</li>
{% endblock %}
{% block content %}
<table class="table">
<thead class="thead-inverse">
<tr>
<th>Board</th>
<th>Posts</th>
<th>Topics</th>
<th>Last Post</th>
</tr>
</thead>
<tbody>
{% for board in boards %}
<tr>
<td>
<a href="{% url 'board_topics' board.pk %}">{{ board.name }}</a>
<small class="text-muted d-block">{{ board.description }}</small>
</td>
<td class="align-middle">
{{ board.get_posts_count }}
</td>
<td class="align-middle">
{{ board.topics.count }}
</td>
<td class="align-middle">
{% with post=board.get_last_post %}
<small>
<a href="{% url 'topic_posts' board.pk post.topic.pk %}">
By {{ post.created_by.username }} at {{ post.created_at }}
</a>
</small>
{% endwith %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}
看看最終結(jié)果:
運(yùn)行測(cè)試用例:
python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.......................................................EEE......................
======================================================================
ERROR: test_home_url_resolves_home_view (boards.tests.test_view_home.HomeTests)
----------------------------------------------------------------------
django.urls.exceptions.NoReverseMatch: Reverse for 'topic_posts' with arguments '(1, '')' not found. 1 pattern(s) tried: ['boards/(?P<pk>\\d+)/topics/(?P<topic_pk>\\d+)/$']
======================================================================
ERROR: test_home_view_contains_link_to_topics_page (boards.tests.test_view_home.HomeTests)
----------------------------------------------------------------------
django.urls.exceptions.NoReverseMatch: Reverse for 'topic_posts' with arguments '(1, '')' not found. 1 pattern(s) tried: ['boards/(?P<pk>\\d+)/topics/(?P<topic_pk>\\d+)/$']
======================================================================
ERROR: test_home_view_status_code (boards.tests.test_view_home.HomeTests)
----------------------------------------------------------------------
django.urls.exceptions.NoReverseMatch: Reverse for 'topic_posts' with arguments '(1, '')' not found. 1 pattern(s) tried: ['boards/(?P<pk>\\d+)/topics/(?P<topic_pk>\\d+)/$']
----------------------------------------------------------------------
Ran 80 tests in 5.663s
FAILED (errors=3)
Destroying test database for alias 'default'...
當(dāng)模板沒(méi)有主題時(shí)應(yīng)用程序崩潰了,看起來(lái)這里有一些實(shí)現(xiàn)上的邏輯問(wèn)題锌俱。
templates/home.html
{% with post=board.get_last_post %}
{% if post %}
<small>
<a href="{% url 'topic_posts' board.pk post.topic.pk %}">
By {{ post.created_by.username }} at {{ post.created_at }}
</a>
</small>
{% else %}
<small class="text-muted">
<em>No posts yet.</em>
</small>
{% endif %}
{% endwith %}
再運(yùn)行一次測(cè)試:
python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
................................................................................
----------------------------------------------------------------------
Ran 80 tests in 5.630s
OK
Destroying test database for alias 'default'...
我創(chuàng)建了一個(gè)新的無(wú)任何主題和帖子的空版塊來(lái)測(cè)試:
現(xiàn)在是時(shí)候改進(jìn)一下主題列表頁(yè)面了晤郑。
這里我會(huì)用另外一種高效的方式來(lái)獲取帖子回復(fù)總數(shù)。
先啟動(dòng)Python終端:
python manage.py shell
from django.db.models import Count
from boards.models import Board
board = Board.objects.get(name='Django')
topics = board.topics.order_by('-last_updated').annotate(replies=Count('posts'))
for topic in topics:
print(topic.replies)
2
4
2
1
這里查詢數(shù)據(jù)集使用了annotate
來(lái)動(dòng)態(tài)生成了一個(gè)屬性列,字段名為replies
造寝,值為Count('posts')
磕洪。這樣就可以直接通過(guò)topic.replies
訪問(wèn)到這個(gè)主題下的帖子總數(shù)了。
這里做一個(gè)小小的優(yōu)化诫龙,回復(fù)總數(shù)的統(tǒng)計(jì)應(yīng)該將帖子里的第一條Post排除在外析显。
We can do just a minor fix because the replies should not consider the starter topic (which is also a Post instance).
我們這樣來(lái)實(shí)現(xiàn)它:
topics = board.topics.order_by('-last_updated').annotate(replies=Count('posts') - 1)
for topic in topics:
print(topic.replies)
1
3
1
0
帥氣吧?!
-
boards/views.py
(完整文檔地址)
from django.db.models import Count
from django.shortcuts import get_object_or_404, render
from .models import Board
def board_topics(request, pk):
board = get_object_or_404(Board, pk=pk)
topics = board.topics.order_by('-last_updated').annotate(replies=Count('posts') - 1)
return render(request, 'topics.html', {'board': board, 'topics': topics})
-
templates/topics.html
(完整文檔地址)
{% for topic in topics %}
<tr>
<td><a href="{% url 'topic_posts' board.pk topic.pk %}">{{ topic.subject }}</a></td>
<td>{{ topic.starter.username }}</td>
<td>{{ topic.replies }}</td>
<td>0</td>
<td>{{ topic.last_updated }}</td>
</tr>
{% endfor %}
下一步讓我們來(lái)處理閱讀的次數(shù)签赃,在此之前我們先創(chuàng)建一個(gè)新的字段谷异。
遷移
遷移是Django進(jìn)行Web開發(fā)的基本功能,通過(guò)它可以使模型的文件與數(shù)據(jù)庫(kù)保持同步锦聊。
當(dāng)我們第一次運(yùn)行python manage.py migrate
時(shí)歹嘹,DJango會(huì)根據(jù)所有的遷移文件來(lái)生成數(shù)據(jù)庫(kù)架構(gòu)。
當(dāng)Django應(yīng)用遷移時(shí)括丁,它有一個(gè)名為Django_migrations
的特殊表荞下,Django在這里記錄所有的遷移伶选。
當(dāng)我們?cè)俅芜\(yùn)行遷移命令:
python manage.py migrate
Operations to perform:
Apply all migrations: admin, auth, boards, contenttypes, sessions
Running migrations:
No migrations to apply.
DJango就能知道現(xiàn)在已經(jīng)沒(méi)有需要變更的內(nèi)容史飞。
讓我們?yōu)?strong>Topic類創(chuàng)建一個(gè)新字段并進(jìn)行遷移吧:
-
boards/models.py
(完整文檔地址)
class Topic(models.Model):
subject = models.CharField(max_length=255)
last_updated = models.DateTimeField(auto_now_add=True)
board = models.ForeignKey(Board, related_name='topics')
starter = models.ForeignKey(User, related_name='topics')
views = models.PositiveIntegerField(default=0) # <- here
def __str__(self):
return self.subject
這里我們使用的是PositiveIntegerField
,那是因?yàn)檫@個(gè)閱讀數(shù)是不可能為負(fù)數(shù)的仰税。
在我們使用這個(gè)新字段前构资,讓我們更新一下數(shù)據(jù)庫(kù)接口,使用makemigrations
命令:
python manage.py makemigrations
Migrations for 'boards':
boards/migrations/0003_topic_views.py
- Add field views to topic
makemigrations
會(huì)自動(dòng)生成一個(gè)0003_topic_views.py的文件陨簇,用來(lái)生成或修改數(shù)據(jù)庫(kù)結(jié)構(gòu)吐绵,這里就會(huì)添加一個(gè)views閱讀數(shù)字段。
讓我們應(yīng)用這個(gè)遷移河绽,運(yùn)行命令migrate
:
python manage.py migrate
Operations to perform:
Apply all migrations: admin, auth, boards, contenttypes, sessions
Running migrations:
Applying boards.0003_topic_views... OK
好了己单,現(xiàn)在我們可以使用閱讀數(shù)字段來(lái)記錄閱讀的總數(shù)了:
-
boards/views.py
(完整文檔地址)
from django.shortcuts import get_object_or_404, render
from .models import Topic
def topic_posts(request, pk, topic_pk):
topic = get_object_or_404(Topic, board__pk=pk, pk=topic_pk)
topic.views += 1
topic.save()
return render(request, 'topic_posts.html', {'topic': topic})
-
templates/topics.html
(完整文檔地址)
{% for topic in topics %}
<tr>
<td><a href="{% url 'topic_posts' board.pk topic.pk %}">{{ topic.subject }}</a></td>
<td>{{ topic.starter.username }}</td>
<td>{{ topic.replies }}</td>
<td>{{ topic.views }}</td> <!-- 這兒 -->
<td>{{ topic.last_updated }}</td>
</tr>
{% endfor %}
現(xiàn)在我們打開一個(gè)主題頁(yè)面,并且刷新幾次耙饰,看看這個(gè)頁(yè)面的閱讀數(shù)是否變化:
小結(jié)
在本教程中纹笼,我們對(duì)Web版塊功能方面進(jìn)行了一些改進(jìn)。還有一些東西需要實(shí)現(xiàn):編輯帖子的頁(yè)面苟跪,個(gè)人資料頁(yè)面并包含修改用戶名功能等等廷痘。在這兩個(gè)視圖之后,我們將在帖子的編輯和查看中使用markdown編輯器件已,還要在主題列表和主題回復(fù)列表中實(shí)現(xiàn)分頁(yè)笋额。
下一個(gè)教程將著重于使用基于類的視圖來(lái)解決這些問(wèn)題。最后篷扩,我們將學(xué)習(xí)如何將應(yīng)用程序部署到Web服務(wù)器上兄猩。
項(xiàng)目的源代碼可以在GitHub上找到。項(xiàng)目的當(dāng)前狀態(tài)可以在發(fā)布標(biāo)簽v0.5-lw下找到。下面的鏈接將帶您找到正確的位置:
https://github.com/sibtc/django-beginners-guide/tree/v0.5-lw
上一節(jié):Django初學(xué)者入門指南4-登錄認(rèn)證(譯&改)
下一節(jié):Django初學(xué)者入門指南6-基于類的頁(yè)面(譯&改)