Django初学者入门指南5-存储数据(译&改)

Django初学者入门指南1-初识(译&改)

Django初学者入门指南2-基础知识(译&改)

Django初学者入门指南3-高级概念(译&改)

Django初学者入门指南4-登录认证(译&改)

Django初学者入门指南5-存储数据(译&改)

Django初学者入门指南6-基于类的页面(译&改)

Django初学者入门指南7-部署发布(译&改)

>>原文地址 By Vitor Freitas

简介

欢迎来到教程系列的第5部分!在本教程中,我们将学习更多用户权限控制的知识,还将实现主题、帖子列表视图和回复页面。还有,我们将探讨Django-ORM的一些特性,并简要介绍迁移。


限制访问页面

我们必须限制未登录用户的页面访问权限,比如下面这个创建新主题的页面:

从导航条可以看出用户没有登录,但是他依然能访问创建新主题的页面:

DJango有一个内置页面装饰器来解决这个问题:

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个文件:

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用来查询主题。

它的页面方法就会是这样的:

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相关,所以我们可以间接访问到当前版块的信息,你可以在下面的代码里看到:

{% 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模板里的链接:

{% 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路由:

<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>

为回复页面创建一个新的表单类:

from django import forms
from .models import Post

class PostForm(forms.ModelForm):
    class Meta:
        model = Post
        fields = ['message', ]

也创建一个新的需要@login_required装饰器的页面,再加上简单的表单处理逻辑:

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 %}

用户回复成功后,会重定向到该主题下的帖子列表页面:

让我们再给第一个帖子加一个效果进行区分:

{% 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.pyboards/tests目录下:

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__方法实现以下。

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

这样就拿到了。

要获取版块下的帖子和回复总数就会稍微麻烦一点,因为BoardPost没有直接关系。

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,对应的就是TopicBoard的关系,这背后DJango给BoardTopicPost建立了一个桥梁关系,构建了一个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>

搞定,我们现在可以开始写代码了。

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

帅气吧?!

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})
{% 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类创建一个新字段并进行迁移吧:

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

好了,现在我们可以使用阅读数字段来记录阅读的总数了:

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})
{% 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 %}

现在我们打开一个主题页面,并且刷新几次,看看这个页面的阅读数是否变化:

Posts

小结

在本教程中,我们对Web版块功能方面进行了一些改进。还有一些东西需要实现:编辑帖子的页面,个人资料页面并包含修改用户名功能等等。在这两个视图之后,我们将在帖子的编辑和查看中使用markdown编辑器,还要在主题列表和主题回复列表中实现分页。

下一个教程将着重于使用基于类的视图来解决这些问题。最后,我们将学习如何将应用程序部署到Web服务器上。

项目的源代码可以在GitHub上找到。项目的当前状态可以在发布标签v0.5-lw下找到。下面的链接将带您找到正确的位置:

https://github.com/sibtc/django-beginners-guide/tree/v0.5-lw

上一节:Django初学者入门指南4-登录认证(译&改)

下一节:Django初学者入门指南6-基于类的页面(译&改)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342