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

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

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


Django 1.10官方文档的入门教程已经翻译完毕,后续的部分将不会按照顺序进行翻译,而是挑重点的先翻译。
有兴趣的能够关注个人博客。css

第一部分传送门html

第二部分传送门python

第四部分传送门程序员

3.2 模型和数据库Models and databasesweb

3.2.2 查询操做making queriesshell

3.3.8 会话sessions数据库

目录

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

  • 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&#39;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”字段前面(默认是在后面)。以下图所示:
1.png-8.9kB

对于只有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)

字段集合中每个元组的第一个元素是该字段集合的标题。它让咱们的页面看起来像下面的样子:
2.png-10kB

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”表单页面看起来以下图:

3.png-6.6kB

在这个表单中,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”页面,应该看到以下图所示:

4.png-20.9kB

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

在3个插槽的最后,还有一个“Add another Choice”连接。点击它,又能够得到一个新的插槽。若是你想删除新增的插槽,点击它右上方的X图标便可。可是,默认的三个插槽不可删除。下面是新增插槽的样子:

5.png-11.3kB

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

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

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

6.png-8.8kB

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

2.9.3 自定义admin change list

Question的admin页面咱们已经修改得差很少了,下面让咱们来微调一下“change list”页面,该页面显示了当前系统中全部的questions。

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

7.png-9.4kB

一般,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()方法的结果也显示出来。如今,页面看起来会是下面的样子:

8.png-11.9kB

你能够点击每一列的标题,来根据这列的内容进行排序。可是,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的过滤面板,以下图所示:

9.png-16.6kB

根据你选择的过滤条件的不一样,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》。

相关文章
相关标签/搜索