“全能”选手—Django 1.10文档中文版Part3

时间:2023-12-15 10:18:32

欢迎大家访问我的个人网站《刘江的博客和教程》:www.liujiangblog.com

主要分享Python 及Django教程以及相关的博客


Django 1.10官方文档的入门教程已经翻译完毕,后续的部分将不会按照顺序进行翻译,而是挑重点的先翻译。

有兴趣的可以关注我的博客。

第一部分传送门

第二部分传送门

第四部分传送门

3.2 模型和数据库Models and databases

3.2.2 查询操作making queries

3.3.8 会话sessions

目录

2.7 第一个Django app,Part 5:测试

  • 2.7.1 自动化测试介绍
  • 2.7.2 基本的测试策略
  • 2.7.3 编写我们的第一个测试程序
  • 2.7.4 测试一个视图
  • 2.7.5 测试越多越好
  • 2.7.6 进一步测试

2.8 第一个Django app,Part 6:静态文件

  • 2.8.1 自定义app的外观
  • 2.8.2 添加背景图片

2.9 第一个Django app,Part 7:自定义admin站点

  • 2.9.1 自定义admin表单
  • 2.9.2 添加关系对象
  • 2.9.3 自定义admin change list
  • 2.9.4 定制admin外观
  • 2.9.5 定制admin首页
  • 2.9.6 接下来学习什么?

2.7 第一个Django app,Part 5:测试

本章承上启下,主要介绍自动化测试相关的内容。

2.7.1 自动化测试介绍

什么是自动化测试

测试是一种例行工作用于检查你的代码的行为。

测试可以划分为不同的级别。一些测试可能专注于小细节(某一个模型的方法是否会返回预期的值?), 一些测试则专注于检查软件的整体运行是否正常(用户在对网站进行了一系列的输入后,是否返回了期望的结果?)。这些其实和你早前在教程2中做的测试差不多,使用shell来检测一个方法的行为,或者运行程序并输入数据来检查它是怎么执行的。

自动化测试的不同之处就在于这些测试会由系统来帮你完成。一旦你创建了一组测试程序,当你修改了你的应用,你就可以用这组测试程序来检查你的代码是否仍然同预期的那样运行,而无需执行耗时的手动测试。

为什么需要测试

那么,为什么要进行测试?而且为什么是现在?

你可能觉得自己的Python/Django能力已经足够,再去学习其他的东西也许不是那么的必要。 毕竟,我们先前联系的投票应用已经表现得挺好了,将时间花在自动化测试上还不如用在改进我们的应用上。 如果你学习Django就是为了创建这么一个简单的投票应用,那么进行自动化测试显然没有必要。 但如果不是这样,那么现在是一个很好的学习机会。

测试可以节省你的时间

某种程度上,“检查并发现工作正常”似乎是种比较满意的测试结果。但在一些复杂的应用中,你会发现组件之间存在各种各样复杂的交互关系。

任何一个组件的改动,都有可能导致应用程序产生无法预料的结果。得出‘似乎工作正常’的结果,可能意味着你需要使用二十种不同的测试数据来测试你的代码,而这仅仅是为了确保你没有搞砸某些事 ,很显然,这种方法效率低下。然而,自动化测试只需要数秒就可以完成以上的任务。如果出现了错误,还能够帮助找出引发这个异常行为的代码。

有时候你可能会觉得编写测试程序相比起有价值的、创造性的编程工作显得单调乏味、无趣,尤其是当你的代码工作正常时。然而,比起用几个小时的时间来手动测试你的程序,或者试图找出代码中一个新生问题的原因,编写测试程序的性价比还是很高的。

(译者:下面都是些测试重要性的论述,看标题就好了)

  • 测试不仅仅可以发现问题,它们还能防止问题
  • 测试使你的代码更受欢迎
  • 测试有助于团队合作

2.7.2 基本的测试策略

编写测试程序有很多种方法。一些程序员遵循一种叫做“测试驱动开发”的规则,他们在编写代码前会先编好测试程序。看起来似乎有点反人类,但实际上这种方法与大多数人经常的做法很相似:先描述一个问题,然后编写代码来解决这个问题。测试驱动开发可以简单地用Python测试用例将问题格式化。

很多时候,刚接触测试的人会先编写一些代码后才编写测试程序。事实上,在之前就编写一些测试会好一点,但不管怎么说什么时候开始都不算晚。

有时候你很难决定从什么时候开始编写测试。如果你已经编写了数千行Python代码,挑选它们中的一些来进行测试是不太容易的。这种情况下,在下次你对代码进行变更,添加一个新功能或者修复一个bug之时,编写你的第一个测试,效果会非常好。

下面,让我们马上来编写一个测试。

2.7.3 编写我们的第一个测试程序

发现BUG

很巧,在我们的投票应用中有一个小bug需要修改:在Question.was_published_recently()方法的返回值中,当Qeustion在最近的一天发布的时候返回True(这是正确的),然而当Question在未来的日期内发布的时候也返回True(这是错误的)。

我们可以在admin后台创建一个发布日期在未来的Question,然后在shell中验证这个bug:

>>> import datetime
>>> from django.utils import timezone
>>> from polls.models import Question
>>> # 创建一个发布日期在30天后的问卷
>>> future_question = Question(pub_date=timezone.now() + datetime.timedelta(days=30))
>>> # 测试一下返回值
>>> future_question.was_published_recently()
True

由于“将来”不等于“最近”,因此这显然是个bug。

创建一个测试来暴露这个bug

刚才我们是在shell中测试了这个bug,那如何通过自动化测试来发现这个bug呢?

通常,我们会把测试代码放在应用的tests.py文件中;测试系统将自动地从任何名字以test开头的文件中查找测试程序。

将下面的代码输入投票应用的tests.py文件中:

polls/tests.py

import datetime
from django.utils import timezone
from django.test import TestCase
from .models import Question class QuestionMethodTests(TestCase):
def test_was_published_recently_with_future_question(self):
"""
在将来发布的问卷应该返回False
"""
time = timezone.now() + datetime.timedelta(days=30)
future_question = Question(pub_date=time)
self.assertIs(future_question.was_published_recently(), False)

我们在这里创建了一个django.test.TestCase的子类,它具有一个方法,该方法创建一个pub_date在未来的Question实例。最后我们检查was_published_recently()的输出,它应该是 False。

运行测试程序

在终端中,运行下面的命令,

$ python manage.py test polls

你将看到结果如下:

Creating test database for alias 'default'...
F
======================================================================
FAIL: test_was_published_recently_with_future_question (polls.tests.QuestionMethodTests)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/path/to/mysite/polls/tests.py", line 16, in test_was_published_recently_with_future_question
self.assertIs(future_question.was_published_recently(), False)
AssertionError: True is not False
----------------------------------------------------------------------
Ran 1 test in 0.001s
FAILED (failures=1)
Destroying test database for alias 'default'...

这其中都发生了些什么?:

  • python manage.py test polls命令会查找所有投票应用中的测试程序
  • 发现一个django.test.TestCase的子类
  • 为测试创建一个专用的数据库
  • 查找函数名以test开头的测试方法
  • 在test_was_published_recently_with_future_question方法中,创建一个Question实例,该实例的pub_data字段的值是30天后的未来日期。
  • 然后利用assertIs()方法,它发现was_published_recently()返回了True,而不是我们希望的False。

这个测试通知我们哪个测试失败了,错误出现在哪一行。

修复bug

我们已经知道了问题所在,现在可以去修复bug了。具体如下:

polls/models.py

def was_published_recently(self):
now = timezone.now()
return now - datetime.timedelta(days=1) <= self.pub_date <= now

再次运行测试程序:

Creating test database for alias 'default'...
.
----------------------------------------------------------------------
Ran 1 test in 0.001s
OK
Destroying test database for alias 'default'...

更加全面的测试

我们可以使was_published_recently()方法更加可靠,事实上,在修复一个错误的同时又引入一个新的错误将是一件很令人尴尬的事。

下面,我们在同一个测试类中再额外添加两个其它的方法,来更加全面地进行测试:

polls/tests.py

def test_was_published_recently_with_old_question(self):
"""
日期超过1天的将返回False。这里创建了一个30天前发布的实例。
"""
time = timezone.now() - datetime.timedelta(days=30)
old_question = Question(pub_date=time)
self.assertIs(old_question.was_published_recently(), False) def test_was_published_recently_with_recent_question(self):
"""
最近一天内的将返回True。这里创建了一个1小时内发布的实例。
"""
time = timezone.now() - datetime.timedelta(hours=1)
recent_question = Question(pub_date=time)
self.assertIs(recent_question.was_published_recently(), True)

现在我们有三个测试来保证无论发布时间是在过去、现在还是未来Question.was_published_recently()都将返回正确的结果。

最后,polls 应用虽然简单,但是无论它今后会变得多么复杂以及会和多少其它的应用产生相互作用,我们都能保证Question.was_published_recently()会按照预期的那样工作。

2.7.4 测试一个视图

这个投票应用没有辨别能力:它将会发布任何的Question,包括pub_date字段是未来的。我们应该改进这一点。让pub_date是将来时间的Question应该在未来发布,但是一直不可见,直到那个时间点才会变得可见。

在我们尝试修复任何事情之前,让我们先看一下可用的工具。

Django测试用客户端

Django提供了一个测试客户端用来模拟用户和代码的交互。我们可以在tests.py甚至shell 中使用它。

先介绍使用shell的情况,这种方式下,需要做很多在tests.py中不必做的事。首先是设置测试环境:

>>> from django.test.utils import setup_test_environment
>>> setup_test_environment()

setup_test_environment()会安装一个模板渲染器,它使我们可以检查一些额外的属性比如response.context,这些属性通常情况下是访问不到的。请注意,这种方法不会建立一个测试数据库,所以以下命令将运行在现有的数据库上,输出的内容也会根据你已经创建的Question的不同而稍有不同。如果你当前settings.py中的的TIME_ZONE不正确,那么你或许得不到预期的结果。在进行下一步之前,请确保时区设置正确。

下面我们需要导入测试客户端类(在之后的tests.py中,我们将使用django.test.TestCase类,它具有自己的客户端,不需要导入这个类):

>>> from django.test import Client
>>> # 创建一个实例
>>> client = Client()

下面是具体的一些使用操作:

>>> # 从'/'获取响应
>>> response = client.get('/')
>>> # 这个地址应该返回的是404页面
>>> response.status_code
404
>>> # 另一方面我们希望在'/polls/'获取一些内容
>>> # 通过使用'reverse()'方法,而不是URL硬编码
>>> from django.urls import reverse
>>> response = client.get(reverse('polls:index'))
>>> response.status_code
200
>>> response.content
b'\n <ul>\n \n <li><a href="/polls/1/">What's up?</a></li>\n \n </ul>\n\n'
>>> # 如果下面的操作没有正常执行,有可能是你前面忘了安装测试环境--setup_test_environment()
>>> response.context['latest_question_list']
<QuerySet [<Question: What's up?>]>

改进我们的视图

投票的列表会显示还没有发布的问卷(即pub_date在未来的问卷)。让我们来修复它。

在教程 4中,我们介绍了一个继承ListView的基类视图:

polls/views.py

class IndexView(generic.ListView):
template_name = 'polls/index.html'
context_object_name = 'latest_question_list' def get_queryset(self):
"""Return the last five published questions."""
return Question.objects.order_by('-pub_date')[:5]

我们需要在get_queryset()方法中对比timezone.now()。首先导入timezone模块,然后修改

get_queryset()方法,如下:

polls/views.py

from django.utils import timezone

def get_queryset(self):
"""
Return the last five published questions (not including those set to be
published in the future).
"""
return Question.objects.filter(
pub_date__lte=timezone.now()
).order_by('-pub_date')[:5]

filter()方法,确保了查询的结果是在当前时间之前,而不包含将来的日期。

测试新视图

对于没有测试概念的程序员,启动服务器、在浏览器中载入站点、创建一些发布时间在过去和将来的Questions,然后检验是否只有已经发布的Question才会展示出来,整个过程耗费大量的时间。对于有测试理念的程序员,不会每次修改与这相关的代码时都重复上述步骤,编写一测试程序是必然的。下面,让我们基于以上shell会话中的内容,再编写一个测试。

将下面的代码添加到polls/tests.py:

首先导入reverse方法:

from django.urls import reverse

创建一个快捷函数来创建Question,同时创建一个新的测试类:

def create_question(question_text, days):
"""
2个参数,一个是问卷的文本内容,另外一个是当前时间的偏移天数,负值表示发布日期在过去,正值表示发布日期在将来。
"""
time = timezone.now() + datetime.timedelta(days=days)
return Question.objects.create(question_text=question_text, pub_date=time) class QuestionViewTests(TestCase):
def test_index_view_with_no_questions(self):
"""
如果问卷不存在,给出相应的提示。
"""
response = self.client.get(reverse('polls:index'))
self.assertEqual(response.status_code, 200)
self.assertContains(response, "No polls are available.")
self.assertQuerysetEqual(response.context['latest_question_list'], []) def test_index_view_with_a_past_question(self):
"""
发布日期在过去的问卷将在index页面显示。
"""
create_question(question_text="Past question.", days=-30)
response = self.client.get(reverse('polls:index'))
self.assertQuerysetEqual(
response.context['latest_question_list'],
['<Question: Past question.>']
) def test_index_view_with_a_future_question(self):
"""
发布日期在将来的问卷不会在index页面显示
"""
create_question(question_text="Future question.", days=30)
response = self.client.get(reverse('polls:index'))
self.assertContains(response, "No polls are available.")
self.assertQuerysetEqual(response.context['latest_question_list'], []) def test_index_view_with_future_question_and_past_question(self):
"""
即使同时存在过去和将来的问卷,也只有过去的问卷会被显示。
"""
create_question(question_text="Past question.", days=-30)
create_question(question_text="Future question.", days=30)
response = self.client.get(reverse('polls:index'))
self.assertQuerysetEqual(
response.context['latest_question_list'],
['<Question: Past question.>']
) def test_index_view_with_two_past_questions(self):
"""
index页面可以同时显示多个问卷。
"""
create_question(question_text="Past question 1.", days=-30)
create_question(question_text="Past question 2.", days=-5)
response = self.client.get(reverse('polls:index'))
self.assertQuerysetEqual(
response.context['latest_question_list'],
['<Question: Past question 2.>', '<Question: Past question 1.>']
)

看一下具体的解释:

create_question是一个创建Question对象的函数。

test_index_view_with_no_questions不创建任何Question,但会检查消息“No polls are available.” 并验证latest_question_list为空。注意django.test.TestCase类提供一些额外的断言方法。在这些例子中,我们使用了assertContains() 和assertQuerysetEqual()。

在test_index_view_with_a_past_question中,我们创建一个Question并验证它是否出现在列表中。

在test_index_view_with_a_future_question中,我们创建一个pub_date在未来的Question。数据库会为每一个测试方法进行重置,所以第一个Question已经不在那里,因此index页面里不应该有任何Question。

诸如此类,事实上,我们是在用测试,模拟站点上的管理员输入和用户体验,检查系统的每一个状态变化,发布的是预期的结果。

测试 DetailView视图

然而,即使未来发布的Question不会出现在index中,如果用户知道或者猜出正确的URL依然可以访问它们。所以我们需要给DetailView视图添加一个这样的约束:

polls/views.py

class DetailView(generic.DetailView):
...
def get_queryset(self):
return Question.objects.filter(pub_date__lte=timezone.now())

同样,我们将增加一些测试来检验pub_date在过去的Question可以显示出来,而pub_date在未来的不可以。

class QuestionIndexDetailTests(TestCase):
def test_detail_view_with_a_future_question(self):
"""
访问发布时间在将来的detail页面将返回404.
"""
future_question = create_question(question_text='Future question.', days=5)
url = reverse('polls:detail', args=(future_question.id,))
response = self.client.get(url)
self.assertEqual(response.status_code, 404) def test_detail_view_with_a_past_question(self):
"""
访问发布时间在过去的detail页面将返回详细问卷内容。
"""
past_question = create_question(question_text='Past Question.', days=-5)
url = reverse('polls:detail', args=(past_question.id,))
response = self.client.get(url)
self.assertContains(response, past_question.question_text)

更多的测试设计

我们应该添加一个类似get_queryset的方法到ResultsView并为该视图创建一个新的类。这将与我们上面的范例非常类似,实际上也有许多重复。

我们还可以在其它方面改进我们的应用,并随之不断地增加测试。例如,发布一个没有Choices的Questions就显得极不合理。所以,我们的视图应该检查这点并排除这些Questions。我们的测试会创建一个不带Choices的Question然后测试它不会发布出来,同时创建一个类似的带有Choices的Question并确保它会发布出来。

也许登陆的管理员用户应该被允许查看还没发布的Questions,但普通访问者则不行。最终要的是:无论添加什么代码来完成这个要求,都需要提供相应的测试代码,不管你是先编写测试程序然后让这些代码通过测试,还是先用代码解决其中的逻辑再编写测试程序来检验它。

从某种程度上来说,你一定会查看你的测试代码,然后想知道你的测试程序是否过于臃肿,我们接着看下面的内容:

2.7.5 测试越多越好

看起来我们的测试代码正在逐渐失去控制。以这样的速度,测试的代码量将很快超过我们的实际应用程序代码量,对比其它简洁优雅的代码,测试代码既重复又毫无美感。

没关系!随它去!大多数情况下,你可以完一个测试程序,然后忘了它。当你继续开发你的程序时,它将始终执行有效的测试功能。

有时,测试程序需要更新。假设我们让只有具有Choices的Questions才会发布,在这种情况下,许多已经存在的测试都将失败:这会告诉我们哪些测试需要被修改,使得它们保持最新,所以从某种程度上讲,测试可以自己测试自己。

在最坏的情况下,在你的开发过程中,你会发现许多测试变得多余。其实,这不是问题,对测试来说,冗余是一件好事。

只要你的测试被合理地组织,它们就不会变得难以管理。 从经验上来说,好的做法是:

  • 为每个模型或视图创建一个专属的TestClass
  • 为你想测试的每一种情况建立一个单独的测试方法
  • 为测试方法命名时最好从字面上能大概看出它们的功能

2.7.6 进一步测试

本教程只介绍了一些基本的测试。还有很多你可以做的工作,许多非常有用的工具可供你使用。

例如,虽然我们的测试覆盖了模型的内部逻辑和视图发布信息的方式,但你还可以使用一个“基于浏览器”的框架例如Selenium来测试你的HTML文件真实渲染的样子。这些工具不仅可以让你检查你的Django代码的行为,还能够检查JavaScript的行为。它会启动一个浏览器,与你的网站进行交互,就像有一个人在操纵一样!Django包含一个LiveServerTestCase来帮助与Selenium 这样的工具集成。

如果你有一个复杂的应用,你可能为了实现持续集成,想在每次提交代码前对代码进行自动化测试,让代码自动至少是部分自动地来控制它的质量。

发现你应用中未经测试的代码的一个好方法是检查代码测试的覆盖率。这也有助于识别脆弱的甚至僵尸代码。如果你不能测试一段代码,这通常意味着这些代码需要被重构或者移除。 覆盖率将帮助我们识别僵尸代码。查看3.9节《Testing in Django》来了解更多细节。

本节介绍了简单的测试方法。下一节我们将介绍静态文件。

2.8 第一个Django app,Part 6:静态文件

前面我们编写了一个经过测试的投票应用,现在让我们给它添加一张样式表和一张图片。

除了由服务器生成的HTML文件外,WEB应用一般需要提供一些其它的必要文件,比如图片文件、JavaScript脚本和CSS样式表等等,用来为用户呈现出一个完整的网页。在Django中,我们将这些文件称为“静态文件”。

对于小项目,这些都不是大问题,你可以将静态文件放在任何你的web服务器能够找到的地方。但是对于大型项目,尤其是那些包含多个app在内的项目,处理那些由app带来的多套不同的静态文件开始变得困难。

但这正是django.contrib.staticfiles的用途:它收集每个应用(和任何你指定的地方)的静态文件到一个单独的地方,并且这个地方在线上可以很容易维护。

2.8.1 自定义app的外观

首先在你的polls目录中创建一个static目录。Django将在那里查找静态文件,这与Django在polls/templates/中寻找对应的模板文件的方式是一致的。

Django的STATICFILES_FINDERS设置项中包含一个查找器列表,它们知道如何从各种源中找到静态文件。 其中一个默认的查找器是AppDirectoriesFinder,它在每个INSTALLED_APPS下查找“static”子目录,例如我们刚创建的那个“static”目录。admin管理站点也为它的静态文件使用相同的目录结构。

在刚才的static中新建一个polls子目录,再在该子目录中创建一个style.css文件。换句话说,这个css样式文件应该是polls/static/polls/style.css。你可以通过书写polls/style.css在Django中访问这个静态文件,与你如何访问模板的路径类似。

静态文件的命名空间:
与模板类似,我们可以将静态文件直接放在polls/static(而不是创建另外一个polls 子目录),但实际上这是一个坏主意。Django将使用它所找到的第一个匹配到的静态文件,如果在你的不同应用中存在两个同名的静态文件,Django将无法区分它们。我们需要告诉Django该使用其中的哪一个,最简单的方法就是为它们添加命名空间。也就是说,将这些静态文件放进以它们所在的应用的名字同名的另外一个子目录下(白话讲:多建一层与应用同名的子目录)。
译者:良好的目录结构是每个应用都应该创建自己的urls、views、models、templates和static,每个templates包含一个与应用同名的子目录,每个static也包含一个与应用同名的子目录。

将下面的代码写入样式文件:

polls/static/polls/style.css

li a {
color: green;
}

接下来在模板文件的头部加入下面的代码:

polls/templates/polls/index.html

{% load static %}
<link rel="stylesheet" type="text/css" href="{% static 'polls/style.css' %}" />

{% static %}模板标签会生成静态文件的绝对URL路径。

重新加载http://localhost:8000/polls/,你会看到Question的超链接变成了绿色(Django风格!),这意味着你的样式表被成功导入。

2.8.2 添加背景图片

下面,我们在polls/static/polls/目录下创建一个用于存放图片的images子目录,在这个子目录里放入background.gif文件。换句话说,这个文件的路径是polls/static/polls/images/background.gif。

修改你的css样式文件:

polls/static/polls/style.css

body {
background: white url("images/background.gif") no-repeat right bottom;
}

重新加载http://localhost:8000/polls/,你会在屏幕的右下方看到载入的背景图片。

警告:
显然,{% static %}模板标签不能用在静态文件,比如样式表中,因为他们不是由Django生成的。 你应该使用相对路径来相互链接静态文件,因为这样你可以改变STATIC_URL ( static模板标签用它来生成URLs)而不用同时修改一大堆静态文件中路径相关的部分。

以上介绍的都是基础中的基础。更多的内容请查看4.15节《Managing static files》和6.5.12节《The staticfiles app》。4.16节《Deploying static files》讨论了更多关于如何在真实服务器上部署静态文件。

本节内容较少,下一节我们将介绍自定义Django的admin站点!

2.9 第一个Django app,Part 7:自定义admin站点

本节我们主要介绍在第二部分简要提到过的Django自动生成的admin站点。

2.9.1 自定义admin表单

通过admin.site.register(Question)语句,我们在admin站点中注册了Question模型。Django会自动生成一个该模型的默认表单页面。如果你想自定义该页面的外观和工作方式,可以在注册对象的时候告诉Django你的选项。

下面是一个修改admin表单默认排序方式的例子:

首先修改admin.py的代码:

polls/admin.py

from django.contrib import admin
from .models import Question class QuestionAdmin(admin.ModelAdmin):
fields = ['pub_date', 'question_text'] admin.site.register(Question, QuestionAdmin)

一般步骤是:创建一个模型管理类,将它作为第二个参数传递给admin.site.register(),随时随地修改模型的admin选项。

上面的修改,让“Publication date”字段显示在“Question”字段前面(默认是在后面)。如下图所示:

“全能”选手—Django 1.10文档中文版Part3

对于只有2个字段的情况,效果看起来还不是很明显,但是,如果,你有一打的字段,选择一种直观符合人类习惯的排序方式是一种重要的有用的细节处理。

同时,谈及包含大量字段的表单,你也许想将表单划分为一些字段集合。

polls/admin.py

from django.contrib import admin
from .models import Question class QuestionAdmin(admin.ModelAdmin):
fieldsets = [
(None, {'fields': ['question_text']}),
('Date information', {'fields': ['pub_date']}),
]
admin.site.register(Question, QuestionAdmin)

字段集合中每一个元组的第一个元素是该字段集合的标题。它让我们的页面看起来像下面的样子:

“全能”选手—Django 1.10文档中文版Part3

2.9.2 添加关系对象

好了,我们已经有了Question的admin页面,一个Question有多个CHoices,但是我们还没有显示Choices的admin页面。有两个办法可以解决这个问题。第一个是像Question一样将Choice注册到admin站点,这很容易:

polls/admin.py

from django.contrib import admin
from .models import Choice, Question # ...
admin.site.register(Choice)

现在访问admin页面,就可以看到Choice了,其“Add Choice”表单页面看起来如下图:

“全能”选手—Django 1.10文档中文版Part3

在这个表单中,Question字段是一个select选择框,包含了当前数据库中所有的Question实例。Django在admin站点中,自动地将所有的外键关系展示为一个select框。在我们的例子中,目前只有一个question对象存在。

请注意图中的绿色加号,它连接到Question模型。每一个包含外键关系的对象都会有这个绿色加号。点击它,会弹出一个新增Question的表单,类似Question自己的添加表单。填入相关信息点击保存后,Django自动将该Question保存在数据库,并作为当前Choice的关联外键对象。白话讲就是,新建一个Question并作为当前Choice的外键。

但是,实话说,这种创建方式的效率不怎么样。如果在创建Question对象的时候就可以直接添加一些Choice,那会更好。让我们来动手试试。

删除Choice模型对register()方法的调用。然后,编辑Question的注册代码如下:

polls/admin.py

from django.contrib import admin
from .models import Choice, Question class ChoiceInline(admin.StackedInline):
model = Choice
extra = 3 class QuestionAdmin(admin.ModelAdmin):
fieldsets = [
(None, {'fields': ['question_text']}),
('Date information', {'fields': ['pub_date'], 'classes': ['collapse']}),
]
inlines = [ChoiceInline] admin.site.register(Question, QuestionAdmin)

上面的代码告诉Django:Choice对象将在Question管理页面进行编辑,默认情况,请提供3个Choice对象的编辑区域。

加载“Add question”页面,应该看到如下图所示:

“全能”选手—Django 1.10文档中文版Part3

它的工作机制是:这里有3个插槽用于关联Choices,而且每当你重新返回一个已经存在的对象的“Change”页面,你又将获得3个新的额外的插槽可用。

在3个插槽的最后,还有一个“Add another Choice”链接。点击它,又可以获得一个新的插槽。如果你想删除新增的插槽,点击它右上方的X图标即可。但是,默认的三个插槽不可删除。下面是新增插槽的样子:

“全能”选手—Django 1.10文档中文版Part3

这里还有点小问题。上面页面中插槽纵队排列的方式需要占据大块的页面空间,查看起来很不方便。为此,Django提供了一种扁平化的显示方式,你仅仅只需要修改一下ChoiceInline继承的类为admin.TabularInline替代先前的StackedInline:

polls/admin.py

class ChoiceInline(admin.TabularInline):
#...

刷新一下页面,你会看到类似表格的显示方式:

“全能”选手—Django 1.10文档中文版Part3

注意“DELETE”列,它可以删除那些已有的Choice和新建的Choice。

2.9.3 自定义admin change list

Question的admin页面我们已经修改得差不多了,下面让我们来微调一下“change list”页面,该页面显示了当前系统中所有的questions。

默认情况下,该页面看起来是这样的:

“全能”选手—Django 1.10文档中文版Part3

通常,Django只显示str()方法指定的内容。但是有时候,我们可能会想要同时显示一些别的内容。要实现这一目的,可以使用list_display属性,它是一个由字段组成的元组,其中的每一个字段都会按顺序显示在“change list”页面上,代码如下:

polls/admin.py

class QuestionAdmin(admin.ModelAdmin):
# ...
list_display = ('question_text', 'pub_date', 'was_published_recently')

额外的,我们把was_published_recently()方法的结果也显示出来。现在,页面看起来会是下面的样子:

“全能”选手—Django 1.10文档中文版Part3

你可以点击每一列的标题,来根据这列的内容进行排序。但是,was_published_recently这一列除外,不支持这种根据函数输出结果进行排序的方式。同时请注意,was_published_recently这一列的列标题默认是方法的名字,内容则是输出的字符串表示形式。

可以通过给方法提供一些属性来改进输出的样式,就如下面所示:

polls/models.py

class Question(models.Model):
# ...
def was_published_recently(self):
now = timezone.now()
return now - datetime.timedelta(days=1) <= self.pub_date <= now
was_published_recently.admin_order_field = 'pub_date'
was_published_recently.boolean = True
was_published_recently.short_description = 'Published recently?'

想要了解更多关于这些方法属性的信息,请参考6.5节《list_displasy》。

我们还可以对显示结果进行过滤,通过使用list_filter属性。在QuestionAdmin中添加下面的代码:

list_filter = ['pub_date']

再次刷新change list页面,你会看到在页面右边多出了一个基于pub_date的过滤面板,如下图所示:

“全能”选手—Django 1.10文档中文版Part3

根据你选择的过滤条件的不同,Django会在面板中添加不容的过滤选项。由于pub_date是一个DateTimeField,因此,Django自动添加了这些选项:“Any date”, “Today”, “Past 7 days”, “This month”, “This year”。

顺理成章的,让我们添加一些搜索的能力:

search_fields = ['question_text']

这会在页面的顶部增加一个搜索框。当输入搜索关键字后,Django会在question_text字段内进行搜索。只要你愿意,你可以使用任意多个搜索字段,Django在后台使用的都是SQL查询语句的LIKE语法,但是,有限制的搜索字段有助于后台的数据库查询效率。

也许你注意到了,页面还提供分页功能,默认每页显示100条。

2.9.4 定制admin外观

很明显,在每一个admin页面顶端都显示“Django administration”是很可笑的,它仅仅是个占位文本。利用Django的模板系统,很容易修改它。

定制你的项目模板

在manage.py文件同级下创建一个templates目录。打开你的设置文件mysite/settings.py,在TEMPLATES条目中添加一个DIRS选项:

mysite/settings.py

TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR, 'templates')],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]

DIRS是一个文件系统目录的列表,是搜索路径。当加载Django模板时,会在DIRS中进行查找。

模板的组织方式:
就像静态文件一样,我们可以把所有的模板都放在一起,形成一个大大的模板文件夹,并且工作正常。但是我们不建议这样!我们建议每一个模板都应该存放在它所属应用的模板目录内(例如polls/templates)而不是整个项目的模板目录(templates),因为这样每个应用才可以被方便和正确的重用。请参考2.10节《如何重用apps》。

接下来,在刚才创建的templates中创建一个admin目录,将admin/base_site.html模板文件拷贝到该目录内。这个html文件来自Django源码,它位于django/contrib/admin/templates目录内。

Django的源代码在哪里?
如果你无法找到Django的源代码文件的存放位置,你可以使用下面的命令:
$ python -c "import django; print(django.__path__)"

编辑该文件,用你喜欢的站点名字替换掉{{ site_header|default:_(’Django administration’) }}(包括两个大括号一起),看起来像下面这样:

{% block branding %}
<h1 id="site-name"><a href="{% url 'admin:index' %}">Polls Administration</a></h1>
{% endblock %}

在这里,我们使用这个方法教会你如何重写模板。但是在实际的项目中,你可以使用django.contrib.admin.AdminSite.site_header属性(详见6.5节),方便的对这个页面title进行自定义。

请注意,所有Django默认的admin模板都可以被重写。类似刚才重写base_site.html模板的方法一样,从源代码目录将html文件拷贝至你自定义的目录内,然后修改文件。

定制你的应用模板

聪明的读者可能会问:但是DIRS默认是空的,Django是如何找到默认的admin模板呢?回答是,由于APP_DIRS被设置为True,Django将自动查找每一个应用包内的templates/子目录(不要忘了django.contrib.admin也是一个应用)。

我们的投票应用不太复杂,因此不需要自定义admin模板。但是如果它变得越来越复杂,因为某些功能而需要修改Django的标准admin模板,那么修改app的模板就比修改项目的模板更加明智。这样的话,你可以将投票应用加入到任何新的项目中,并且保证能够找到它所需要的自定义模板。

查看3.5节《template loading documentation》获取更多关于Django如何查找模板的信息。

2.9.5 定制admin首页

默认情况下,admin首页显示所有INSTALLED_APPS内并在admin应用中注册过的app,以字母顺序进行排序。

要定制admin首页,你需要重写admin/index.html模板,就像前面修改base_site.html模板的方法一样,从源码目录拷贝到你指定的目录内。编辑该文件,你会看到文件内使用了一个app_list模板变量。该变量包含了所有已经安装的Django应用。你可以硬编码链接到指定对象的admin页面,使用任何你认为好的方法,用于替代这个app_list。

2.9.6 接下来学习什么?

至此,新手教程已经结束了。此时,你也许想看看2.11节的《下一步干什么》。

或者你对Python包机制很熟悉,对如何将投票应用转换成一个可重用的app感兴趣,请看2.10节《高级教程:如何编写可重用的apps》。