Django初學(xué)者入門指南5-存儲(chǔ)數(shù)據(jù)(譯&改)

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ā)布(譯&改)

>>原文地址 By Vitor Freitas

簡(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)題:

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è)文件:

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ì)是這樣的:

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)前版塊的信息漓滔,你可以在下面的代碼里看到:

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

{% 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)試了躯舔。

回復(fù)頁(yè)面的線框圖

配置新的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>

為回復(fù)頁(yè)面創(chuàng)建一個(gè)新的表單類:

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)單的表單處理邏輯:

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ū)分:

{% 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.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這個(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)以下。

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)的就是TopicBoard的關(guān)系铭拧,這背后DJango給BoardTopic盆昙、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)在可以開始寫代碼了。

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

帥氣吧?!

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

下一步讓我們來(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)行遷移吧:

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ù)了:

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

現(xiàn)在我們打開一個(gè)主題頁(yè)面,并且刷新幾次耙饰,看看這個(gè)頁(yè)面的閱讀數(shù)是否變化:

Posts

小結(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è)面(譯&改)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末枢冤,一起剝皮案震驚了整個(gè)濱河市援岩,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌掏导,老刑警劉巖享怀,帶你破解...
    沈念sama閱讀 216,496評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異趟咆,居然都是意外死亡添瓷,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,407評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門值纱,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)鳞贷,“玉大人,你說(shuō)我怎么就攤上這事虐唠〔罄ⅲ” “怎么了?”我有些...
    開封第一講書人閱讀 162,632評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵疆偿,是天一觀的道長(zhǎng)咱筛。 經(jīng)常有香客問(wèn)我,道長(zhǎng)杆故,這世上最難降的妖魔是什么迅箩? 我笑而不...
    開封第一講書人閱讀 58,180評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮处铛,結(jié)果婚禮上饲趋,老公的妹妹穿的比我還像新娘。我一直安慰自己撤蟆,他們只是感情好奕塑,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,198評(píng)論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著家肯,像睡著了一般龄砰。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上息楔,一...
    開封第一講書人閱讀 51,165評(píng)論 1 299
  • 那天寝贡,我揣著相機(jī)與錄音,去河邊找鬼值依。 笑死圃泡,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的愿险。 我是一名探鬼主播颇蜡,決...
    沈念sama閱讀 40,052評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼价说,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了风秤?” 一聲冷哼從身側(cè)響起鳖目,我...
    開封第一講書人閱讀 38,910評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎缤弦,沒(méi)想到半個(gè)月后领迈,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,324評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡碍沐,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,542評(píng)論 2 332
  • 正文 我和宋清朗相戀三年狸捅,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片累提。...
    茶點(diǎn)故事閱讀 39,711評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡尘喝,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出斋陪,到底是詐尸還是另有隱情朽褪,我是刑警寧澤,帶...
    沈念sama閱讀 35,424評(píng)論 5 343
  • 正文 年R本政府宣布无虚,位于F島的核電站缔赠,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏骑科。R本人自食惡果不足惜橡淑,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,017評(píng)論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望咆爽。 院中可真熱鬧,春花似錦置森、人聲如沸斗埂。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,668評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)呛凶。三九已至,卻和暖如春行贪,著一層夾襖步出監(jiān)牢的瞬間漾稀,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,823評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工建瘫, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留崭捍,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,722評(píng)論 2 368
  • 正文 我出身青樓啰脚,卻偏偏與公主長(zhǎng)得像殷蛇,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,611評(píng)論 2 353