搭建好工作环境后,我们就可以把目光聚焦到创建站点主页这事儿上啦。

然而,这将不会在是一个只有“hello world”的空白主页。

我们将配置static和templates文件,并使用HTML5 样板文件Twitter Bootstrap来创建一个更好的主页(当然,它也会有hello world)

另外,我们将遵循Testing Goat,按照TDD的理念来创建主页。通过本教程你将学到很多东西! :-)

我们将讲述下列内容:


静态文件配置

打开通用的settings.py文件(即base.py),找到INSTALLED_APPS变量。确保django.contrib.staticfiles在这个变量里。

然后跳转到文件最后,找到下面的内容:

1
STATIC_URL = '/static/'

这行代码告诉Django去每一个app的static文件夹下找静态文件。

然而,有些静态文件在整个项目里可以通用,所以它们不应该搁在特定的app里。进入taskbuster文件夹,在其中创建一个名为static的文件夹

1
2
$ cd taskbuster
$ mkdir static

这个目录中将包含所有项目通用的静态文件,比如CSS或javascript文件。

如果你跳转到文件开头,你可以找到:

1
BASE_DIR = os.path.dirname(os.path.dirname(__file__))

这个变量指向实际文件的祖父目录,例如这里是taskbuster文件夹。

注意:如果你在taskbuster下使用单独的settings.py(即没有settings文件夹),那么你需要重新定义BASE_DIR,以指向taskbuster文件夹。这是因为没有额外的settings文件夹在中间,之前的BASE_DIR将指向taskbuster_project文件夹,而不是taskbuster文件夹。你应该这样重定义它:

1
BASE_DIR = os.path.dirname(os.path.abspath(__file__))

为了告诉Django到我们刚创建的taskbuster/static目录下找静态文件,请在STATIC_URL后面添加下面的代码:

1
2
3
STATICFILES_DIRS = (
os.path.join(BASE_DIR, "static"),
)

别忘了后面的逗号,,Django会从每一个app下的static文件夹和taskbuster/static中寻找静态文件。

模板设置

我们需要对templates做类似的事情。默认地,Django模板加载器将从每一个app下的templates文件夹寻找模板。

但我们先在taskbuster文件夹下创建用于整个项目使用的通用文件的模板文件夹,如base.html或一些错误页面。

1
2
$ cd taskbuster
$ mkdir templates

然后,更新设置文件(base.py),并在TEMPLATE变量里更改DIRS键:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Templates files
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',
],
},
},
]

跟静态文件一样,Django将从每一个app的templates文件夹下和taskbuster/templates寻找模板文件。

Initializr: HTML5 样板文件 和 Twitter Bootstrap

为了测试下模板和静态文件是否能正常工作,并且也因为我喜欢在开发Django时使用一些CSS,我们将使用HTML5 样板文件Bootstrap。这些工具将帮你创建能在不同的浏览器下工作的模板。

这里,我们选择使用Initializr,一个包含HTML5 样板文件Bootstrap的混合版本。进入其官网,选择你的项目所需要的内容。本项目使用的配置如图所示(在本项目中不会用到Apple Touch Icons)

Initializr

下载了压缩包后,将其解压,并重新组织它的内容:

以TDD方式构建主页–测试先行

要确定静态文件和模板是否成功加载,我们需要一个测试。而你知道的…听Testing Goat的话!测试先行,测试先行!

Obey the testing goat

实际上,真正的TDD开发,我们在设置模板和静态文件之前就应该进行测试。但我想先把配置文件完成。

首先,我们需要把functional_tests文件夹变成一个包,很简单,往里面添加一个__init__.py文件即可。

1
$ touch functional_tests/__init__.py

这样,我们就可以通过如下方式运行我们的功能测试了:

1
$ python manage.py test functional_tests

然而,测试工具只会找那些以test开头的文件来运行,因此我们需要把all_user.py重命名为test_all_users.py

我们让git来做这事,这样方便仓库能够正确地探测到这些改变:

1
$ git mv functional_lists/all_users.py functional_tests/test_all_users.py

tb_dev环境下运行开发服务器,然后在tb_test环境下运行这些测试。它们应该跟往常一样正常运行,没有什么有问题的地方!

但我知道你肯定不喜欢同时在两个环境下干活,对吧?我的意思是说在tb_dev下运行服务器,在tb_test下运行测试。为什么不能在测试环境下运行服务器呢?

另外,这些功能测试造成的变化是持久的。想象一下在某一个测试中,我们创建一个model的实例(比如,一个新用户)。我们希望在测试完成后,这个用户实例可以从我们的数据库中消失,对吧?但是对于这些功能测试,我们只是运行了开发服务器,并捣腾了下开发数据库,因此这些变化在测试完成后还将存留。

但不用担心,LiveServerTestCase类会让我们的生活更轻松! :-)

我们将会看到,这个类的实例将会创建一个带有测试数据库的服务器,就像我们运行单元测试一样。

现在,让我们编辑functional_tests/test_all_users.py,看看模板和静态文件是否正常工作。例如,我们可以测试这俩不同的事情。

那我们就开始创建这个测试吧!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# -*- coding: utf-8 -*-
from selenium.webdriver.firefox import webdriver
from django.core.urlresolvers import reverse
from django.contrib.staticfiles.testing import LiveServerTestCase


class HomeNewVisitorTest(LiveServerTestCase):

def setUp(self):
self.browser = webdriver.WebDriver()
self.browser.implicitly_wait(3)

def tearDown(self):
self.browser.quit()

def get_full_url(self, namespace):
return self.live_server_url + reverse(namespace)

def test_home_title(self):
self.browser.get(self.get_full_url("home"))
self.assertIn("TaskBuster", self.browser.title)

def test_h1_css(self):
self.browser.get(self.get_full_url("home"))
h1 = self.browser.find_element_by_tag_name("h1")
self.assertEqual(h1.value_of_css_property("color"),
"rgba(200, 50, 255, 1)")

现在我们来过一遍这些代码:

创建完我们的测试,TDD告诉我们进行下面的循环操作:

我们必须遵从这些循环操作,直到测试通过。在下个章节你将对这些概念有个更清楚的认识。

以TDD方式构建主页–然后编码

现在我们已经为我们的主页创建了功能测试,我们运行下它,看看它是怎么失败的。在tb_test环境下:

1
$ python manage.py test functional_tests

我们可以看到第一个错误是“home”没有定义。打开taskbuster/urls.py,然后从views.py中导入home

1
from .views import home

注意这里我们使用了一个相对导入来导入home视图。这样我们可以不用担心当改变我们项目或应用名称时会破坏urls。

下一步,添加下面的url:

1
2
3
4
5
urlpatterns = [
...
url(r'^$', home, name='home'),
...
]

如果再次运行测试,它仍将失败,因为我们还没有定义任何的home视图。我们来定义一个简单的,打开taskbuster/views.py,写入:

1
2
def home(request):
return ""

仍然会有一个测试失败,因为我们主页标题中没有TaskBuster

现在让我们开始捣腾咱们的模板:用浏览器打开taskbuster/templates/base.html,看看它长个啥样。很恶心对不对?这是因为我们的静态文件还没有加载。

base.html将作为我们的基础模板,其他项目的模板将继承它,包括主页。

那么,让我们开始单元测试吧。额,我知道你只想写个主页模板代码,不是你知道的,我们要听Testing Goat的话! :-)

单元测试是用来从开发者的角度来测试代码的小片段。比如,我们都知道用户并不关心主页模板是不是继承至其他模板,只要他能看到他想看的内容就好。但是开发者关心这事儿,所以我们应该写单元测试。并且我意识到当我必须思考测试的时候,我写出的代码更整洁。我想这是因为写测试能让你思考你的代码真正想做的事。而这将使你头脑清醒。:-)

taskbuster文件夹下创建test.py文件,并写入下面的测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# -*- coding: utf-8 -*-
from django.test import TestCase
from django.core.urlresolvers import reverse


class TestHomePage(TestCase):

def test_uses_index_template(self):
response = self.client.get(reverse("home"))
self.assertTemplateUsed(response, "taskbuster/index.html")

def test_uses_base_template(self):
response = self.client.get(reverse("home"))
self.assertTemplateUsed(response, "base.html")

你可以这样运行这些测试:

1
$ python manage.py test taskbuster.test

很显然,它们将失败…首先,让我们创建taskbuster/index.html模板:

1
2
3
$ cd taskbuster/templates
$ mkdir taskbuster
$ touch taskbuster/index.html

并编辑taskbuster/views.py

1
2
3
4
5
# -*- coding: utf-8 -*-
from django.shortcuts import render

def home(request):
return render(request, "taskbuster/index.html", {})

代码里面的render方法,可以让你加载一个模板,创建一个可以添加一堆变量的上下文,比如当前登录用户的信息,或者当前的语言,然后渲染它,并返回一个HttpResponse对象,这一切都在render方法里实现。注意:默认添加的信息依赖于你在设置文件中设置的模板上下文处理器。

如果你再次运行单元测试,你将发现第一个测试通过了,这表明主页已经使用了taskbuster/index.html模板。我们只需要让这个模板继承至base.html模板。

那么让我们编辑下base.html。现在,我们只对head标签里面的title标签感兴趣。在文件中找到它,并写下下面的内容:

1
2
3
4
5
<head>
...
<title>{% block head_title %}{% endblock %}</title>
...
</head>

{% block head_title %}{% endblock %}这两个模板标签用来标记内容块的起始,这些内容可以在子模板中被替换掉。下一分钟你就回明白这一点了。

再次编辑index.html,让它继承至base.html,然后给它加个标题:

1
2
{% extends "base.html" %}
{% block head_title %}TaskBuster Django Tutorial{% endblock %}

index.html包含base.html除了有这些特殊标签的代码块之外的所有内容。这种情况下,它把index.html的模板标签内容替换成base.html中相应的代码块即可。

让我们再次运行单元测试:

1
$ python manage.py test taskbuster.test

很好…!!太棒了,测试全都通过了!

那么功能测试呢?

1
$ python manage.py test functional_tests

一个通过,一个失败。这有问题呀!但是我们还是需要处理下静态文件先!

首先,让我们自定义我们的CSS文件,编辑taskbuster/static/main.css文件,添加:

1
2
3
.jumbotron h1 {
color: rgba(200, 50, 255, 1);
}

然后,再次编辑base.html,把下面的代码加入到文件开头(甚至可以在<!DOCTYPE html>声明之前):

1
{% load staticfiles %}

然后,找到所有链接静态文件的标签以及包含javascript的脚本标签:

1
2
<link rel="stylesheet" href="css/xxx.css">
<script src="js/xxx.js"></script>

把它们变成这个样子:

1
2
<link rel="stylesheet" href="{% static 'css/xxx.css' %}">
<script src="{% static 'js/xxx.js' %}">

小心处理""''。另外,在文件最后你将看到这样的东西:

1
document.write('<script src="js/vendor/jquery-1.11.0.min.js"><\/script>')</script>

这里我们不能添加{% static "xxx" %}标签,因为这会破坏它所在的字符串。这种情况,你可以加入静态文件指定相对路径:

1
document.write('<script src="static/js/vendor/jquery-1.11.0.min.js"><\/script>')</script>

注意:虽然导入静态文件的方法都可以正常工作,但如果你计划使用内容分发网络(CDN)来处理静态文件,那么你最好使用模板标签。

Ok,让我们再次运行测试!Oh 不!我遇到了一个之前没有的错误!

这是因为LiveServerTestCase不支持静态文件…

但请不用担心,跟往常一样,Django自有妙计!我有另外一个测试类可以支持静态文件!

编辑functional_tests/test_all_users.py文件,去除带有-号的行,增加带有+号的行:

1
2
3
4
5
- from django.test import LiveServerTestCase
+ from django.contrib.staticfiles.testing import StaticLiveServerTestCase

- class HomeNewVisitorTest(LiveServerTestCase):
+ class HomeNewVisitorTest(StaticLiveServerTestCase):

再次运行你的测试,啊哈,它们都通过啦! :-)

如果你要同时运行单元测试和功能测试,你可以这样子:

1
$ python manage.py test

你可以打开本地链接,看看正确加载了CSS文件的主页是多么的漂亮!:-)

…虽然对于h1的字体颜色来说可能不是最好的选择!

注意:如果你运行功能测试,遇到说浏览器在连接之前已经退出了的错误信息,请尝试在你的工作环境中升级selenium:

1
$ pip install -U selenium

这应该可以解决你的问题 :-)

再次把代码提交到本地仓库和Bitbucket

是时候进行下一个提交了!

1
2
$ git add .
$ git status

确保你只添加了你想提交的文件。另外,在输出的开头会显示:

1
Your branch is up-to-date with 'origin/master'

这意味着实际的主分支更Bitbucket中的原始分支状态一致。让我们看看在提交新的改变之后会发生什么:

1
$ git commit -m "Settings, static files and templates"

让我们再次查看状态…

1
2
3
4
5
6
$ git status
On branch master
Your branch is ahead of 'origin/master' by 1 commit.
(use "git push" to publish your local commits)

nothing to commit, working directory clean

因此本地主分支领先原始主分支一个提交。让我们把最近的提交推送到Bitbucket仓库来修复这个问题:

1
$ git push origin master

那么现在我们的分支又跟Bitbucket上的origin/master同步了。

这部分要讲的就这么多,我们已经创建了一个不错的主页啦!

在下一部分,我们将讨论配置其他从Initializr包下载的文件:robots.txthumans.txtfavicon.ico

另外,我将告诉你如果使用coverage,一个用于衡量测试覆盖的代码量工具。

下一部分:网站文件和使用coverage进行测试

请记得跟你的朋友分享本教程! :-)