简介
欢迎来到教程系列的第5部分!在本教程中,我们将学习更多用户权限控制的知识,还将实现主题、帖子列表视图和回复页面。还有,我们将探讨Django-ORM的一些特性,并简要介绍迁移。
限制访问页面
我们必须限制未登录用户的页面访问权限,比如下面这个创建新主题的页面:
从导航条可以看出用户没有登录,但是他依然能访问创建新主题的页面:
DJango有一个内置页面装饰器来解决这个问题:
-
boards/views.py
(完整文档地址)
from django.contrib.auth.decorators import login_required
@login_required
def new_topic(request, pk):
# ...
好了,现在用户打开这个链接就会被重定向到登录页面:
注意这个URL请求的参数?next=/boards/1/new/,这里我们可以通过改进登录页面响应next参数来改进用户体验。
配置登录页面的重定向
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>
现在如果再进行登录,登录成功后就会回到我们弹出登录时我们想去的页面了。
next这个参数是登录页面的一个内置功能。
访问权限测试
现在让我们写一下@login_required
装饰器是否正常工作。在此之前我们先重构一下boards/tests/test_views.py
文件。
让我们把test_views.py
拆成3个文件:
-
test_view_home.py
拷贝的是HomeTests测试类的内容(完整文档地址) -
test_view_board_topics.py
拷贝的是BoardTopicsTests测试类的内容(完整文档地址) -
test_view_new_topic.py
拷贝的是NewTopicTests测试类的内容(完整文档地址)
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/
尝试运行一下测试用例看一切是否都正常。
我们再创建一个新的测试文件test_view_new_topic.py
来测试@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))
上面的测试用例里我们尝试在未登录的情况下去访问创建新主题页面new topic,期望的结果是被重定位到登录页面。
已授权用户的页面访问
现在让我们来改进一下new_topic
,由已登录用户进行操作而不是从数据库中暴力获取第一个用户。之前是临时的调试方法,现在我们有办法进行权限控制了,那么就修正它吧:
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})
我们可以添加一个新主题来测试一下:
主题的帖子列表
现在我们根据下面的线框图来实现这个页面吧:
要想富,先修路:
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>
注意这里我们定义了两个参数: pk
用来查询版块,然后还有一个 topic_pk
用来查询主题。
它的页面方法就会是这样的:
-
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})
注意这里我们没有直接获取Board实例,因为主题Topic与版块Board相关,所以我们可以间接访问到当前版块的信息,你可以在下面的代码里看到:
-
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
来获取版块的名称,而不是通过board.name
获取。
让我们再为topic_posts
页面写一点测试用例吧:
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
方法变得越来越复杂,我们可以利用混合机制或者抽象类来重用代码。或者也可以使用第三方库来初始化测试代码,减少这些样板代码。
同样的,现在我们项目的测试用例也越来越多,执行起来也越来越久,这里可以通过指定应用程序来进行测试:
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'...
也可以指定某一个测试文件来进行测试:
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'...
甚至指定某一个测试用例的方法进行测试:
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
里,我们利用一个循环来枚举这个主题的所有帖子:
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 %}
鉴于目前我们还没有准备上传用户头像图片的功能,先放一张站位图吧。
我下载的免费图片来自IconFinder,把它放到项目里的static/img目录下。
现在我们还没有涉及到DJango的关系对象映射(ORM),实际上{{ post.created_by.posts.count }}
就是在数据库中行类似select count
这样的SQL,通过这样的代码虽然可以得到正确的结果,但是这实际上是一种非常糟糕的写法,因为这里会对数据库进行多次没必要的查询操作。让我们先忽略它,当下先集中精力到程序交互的实现上,稍后再对数据库查询这里的代码进行优化,解决重复查询的问题。
这里还有一个点事我们检测这个帖子是否是当前登录的用户发的:{% if post.created_by == user %}
,如果是的,那么就会在帖子上显示出编辑按钮。
再更新一下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 %}
回复帖子页面
让我们实现回复帖子页面,这样就可以创建更多帖子来调试了。
配置新的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>
为回复页面创建一个新的表单类:
-
boards/forms.py
(完整文档地址)
from django import forms
from .models import Post
class PostForm(forms.ModelForm):
class Meta:
model = Post
fields = ['message', ]
也创建一个新的需要@login_required
装饰器的页面,再加上简单的表单处理逻辑:
-
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
页面方法里标记了# 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 ...
重点: reply_topic页面方法我们使用的参数是topic_pk
,引用的是方法关键字参数;在new_topic页面我们使用的是topic.pk
,引用的是Topic实例的属性。很小的细节,但有很重要。
模板的第一个版本:
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 %}
用户回复成功后,会重定向到该主题下的帖子列表页面:
让我们再给第一个帖子加一个效果进行区分:
-
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 %}
现在再创建一些测试用例,就按我们之前的方式创建就可以了。创建一个新文件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这个测试用例,其他的测试用例预留在这里,后面再使用。
我们先测试页面的装饰器@login_required
是否正常工作,然后检测HTML的输入、请求返回状态码,最后再测试一下提交有效和无效的表单。
查询数据集
现在让我们花点时间来了解DJango模型类的API功能,首先改进一下首页页面:
这里有三个点需要完成:
- 显示版块下的帖子总数;
- 显示版块下的主题总数;
- 显示最新的帖子的作者和发帖时间。
编写代码前,让我们先启动Python终端来进行调试。
在我们开始用终端调试前,先把所有的类的__str__
方法实现以下。
-
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工具类来截断处理帖子消息内容,这样可以很方便的将长文本截断为任意长度。
让我们打开Python终端:
python manage.py shell
# First get a board instance from the database
board = Board.objects.get(name='Django')
这三个点里最容易的就是获取当前主题的数量,因为可以从Board关联的Topic上直接获取到:
board.topics.all()
<QuerySet [<Topic: Hello everyone!>, <Topic: Test>, <Topic: Testing a new post>, <Topic: Hi>]>
board.topics.count()
4
这样就拿到了。
要获取版块下的帖子和回复总数就会稍微麻烦一点,因为Board与Post没有直接关系。
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个帖子,但是这里看不出来哪些是属于DJango
版块的。
需要通过下面的代码筛选出我们需要的结果:
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
,对应的就是Topic和Board的关系,这背后DJango给Board、Topic、Post建立了一个桥梁关系,构建了一个SQL查询来检索指定版块的帖子。
最后我们需要获取到最新的一个帖子实例。
# 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>
搞定,我们现在可以开始写代码了。
-
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
这个参数,因为这个方法是由一个Board实例调用的,所以这个参数的意思就是获取自己这个Board实例,再用这个实例去过滤请求的数据集。
改进一下首页的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 %}
看看最终结果:
运行测试用例:
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'...
当模板没有主题时应用程序崩溃了,看起来这里有一些实现上的逻辑问题。
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 %}
再运行一次测试:
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'...
我创建了一个新的无任何主题和帖子的空版块来测试:
现在是时候改进一下主题列表页面了。
这里我会用另外一种高效的方式来获取帖子回复总数。
先启动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
这里查询数据集使用了annotate
来动态生成了一个属性列,字段名为replies
,值为Count('posts')
。这样就可以直接通过topic.replies
访问到这个主题下的帖子总数了。
这里做一个小小的优化,回复总数的统计应该将帖子里的第一条Post排除在外。
We can do just a minor fix because the replies should not consider the starter topic (which is also a Post instance).
我们这样来实现它:
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 %}
下一步让我们来处理阅读的次数,在此之前我们先创建一个新的字段。
迁移
迁移是Django进行Web开发的基本功能,通过它可以使模型的文件与数据库保持同步。
当我们第一次运行python manage.py migrate
时,DJango会根据所有的迁移文件来生成数据库架构。
当Django应用迁移时,它有一个名为Django_migrations
的特殊表,Django在这里记录所有的迁移。
当我们再次运行迁移命令:
python manage.py migrate
Operations to perform:
Apply all migrations: admin, auth, boards, contenttypes, sessions
Running migrations:
No migrations to apply.
DJango就能知道现在已经没有需要变更的内容。
让我们为Topic类创建一个新字段并进行迁移吧:
-
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
,那是因为这个阅读数是不可能为负数的。
在我们使用这个新字段前,让我们更新一下数据库接口,使用makemigrations
命令:
python manage.py makemigrations
Migrations for 'boards':
boards/migrations/0003_topic_views.py
- Add field views to topic
makemigrations
会自动生成一个0003_topic_views.py的文件,用来生成或修改数据库结构,这里就会添加一个views阅读数字段。
让我们应用这个迁移,运行命令migrate
:
python manage.py migrate
Operations to perform:
Apply all migrations: admin, auth, boards, contenttypes, sessions
Running migrations:
Applying boards.0003_topic_views... OK
好了,现在我们可以使用阅读数字段来记录阅读的总数了:
-
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 %}
现在我们打开一个主题页面,并且刷新几次,看看这个页面的阅读数是否变化:
小结
在本教程中,我们对Web版块功能方面进行了一些改进。还有一些东西需要实现:编辑帖子的页面,个人资料页面并包含修改用户名功能等等。在这两个视图之后,我们将在帖子的编辑和查看中使用markdown编辑器,还要在主题列表和主题回复列表中实现分页。
下一个教程将着重于使用基于类的视图来解决这些问题。最后,我们将学习如何将应用程序部署到Web服务器上。
项目的源代码可以在GitHub上找到。项目的当前状态可以在发布标签v0.5-lw下找到。下面的链接将带您找到正确的位置:
https://github.com/sibtc/django-beginners-guide/tree/v0.5-lw