Django入门学习Day10:URL 调度

前言

在本节中,我们将深入理解两个基本概念: URLs 和 Forms。在这个过程中,我们还将学习其它很多概念,如创建可重用模板和安装第三方库。同时我们还将编写大量单元测试。

URLs

我们的开发就从这里开始
随着我们项目的开发,我们需要实现一个新的功能,就是列出某个板块下的所有主题列表,再来回顾一下,你可以看到上一节中我们画的线框图。

我们将从 myproject 目录中编写 urls.py 开始:

myproject/urls.py

from django.conf.urls import url
from django.contrib import admin

from boards import views

urlpatterns = [
    url(r'^$', views.home, name='home'),
    url(r'^boards/(?P<pk>\d+)/$', views.board_topics, name='board_topics'),
    url(r'^admin/', admin.site.urls),
]

现在我们花点时间来分析一下 urlpatternsurl

URL 调度器(dispatcher)URLconf (URL configuration) 是 Django 应用中的基础部分。在开始的时候,这个看起来让人很困惑。

事实上,Django开发团队正在致力于将路由语法简化(注:就是将原来url函数替换成 path 函数,目前django2.0已经正式使用新的路由语法)

一个项目可以有很多 urls.py 分布在多个应用(app)中。Django 需要一个 urls.py 作为入口。这个特殊的 urls.py 叫做 根路由配置(root URLconf)。它被定义在 settings.py 中。

myproject/settings.py

ROOT_URLCONF = 'myproject.urls'

它已经自动配置好了,你不需要去改变它任何东西。

当 Django 接受一个请求(request), 它就会在项目的 URLconf 中寻找匹配项。他从 urlpatterns 变量的第一条开始,然后在每个 url 中去匹配请求的 URL。

如果 Django 找到了一个匹配路径,他会把请求(request)发送给 url 的第二个参数 视图函数(view function)urlpatterns中的顺序很重要,因为 Django 一旦找到匹配就会停止往后搜索。如果 Django 在 URLconf 中没有找到匹配项,他会通过 Page Not Found 的错误处理代码抛出一个 404 异常。

这是 url 函数的剖析:

def url(regex, view, kwargs=None, name=None):
    # ...
  • regex: 匹配 URL patterns 的正则表达式。注意:正则表达式会忽略掉 GET 或者 POST 后面的参数。在一个 http://127.0.0.1:8000/boards/?page=2 的请求中,只有 /boards/ 会被处理。
  • view: 视图函数被用来处理用户请求,同时它还可以是 django.conf.urls.include 函数的返回值,它将引用一个外部的urls.py文件,例如,你可以使用它来定义一组特定于应用的 URLs,使用前缀将其包含在根 URLconf 中。我们会在后面继续探讨这个概念。
  • kwargs:传递给目标视图函数的任意关键字参数,它通常用于在可重用视图上进行一些简单的定制,我们不是经常使用它。
  • name:: 该 URL 的唯一标识符。这是一个非常重要的特征。要始终记得为你的 URLs 命名。所以,很重要的一点是:不要在 views(视图) 或者 templates(模板) 中硬编码 URL,而是通过它的名字去引用 URL。

基础 URLs 路由

基础URL创建起来很容易。就只是个匹配字符串的问题。比如说,我们想创建一个 "about" 页面,可以这样定义:

from django.conf.urls import url
from boards import views

urlpatterns = [
    url(r'^$', views.home, name='home'),
    url(r'^about/$', views.about, name='about'),
]

我们也可以创建更深层一点的 URL 结构

from django.conf.urls import url
from boards import views

urlpatterns = [
    url(r'^$', views.home, name='home'),
    url(r'^about/$', views.about, name='about'),
    url(r'^about/company/$', views.about_company, name='about_company'),
    url(r'^about/author/$', views.about_author, name='about_author'),
    url(r'^about/author/vitor/$', views.about_vitor, name='about_vitor'),
    url(r'^about/author/erica/$', views.about_erica, name='about_erica'),
    url(r'^privacy/$', views.privacy_policy, name='privacy_policy'),
]

这是一些简单的 URL 路由的例子,对于上面所有的例子,视图函数都是下面这个结构:

def about(request):
    # do something...
    return render(request, 'about.html')

def about_company(request):
    # do something else...
    # return some data along with the view...
    return render(request, 'about_company.html', {'company_name': 'Simple Complex'})

高级 URLs 路由

更高级的URL路由使用方法是通过正则表达式来匹配某些类型的数据并创建动态 URL

例如,要创建一个个人资料的页面,诸如 github.com/vitorfs or twitter.com/vitorfs(vitorfs 是我的用户名) 这样,我们可以像以下几点这样做:

from django.conf.urls import url
from boards import views

urlpatterns = [
    url(r'^$', views.home, name='home'),
    url(r'^(?P<username>[\w.@+-]+)/$', views.user_profile, name='user_profile'),
]

它会匹配 Django 用户模型里面所有有效的用户名。

现在我们可以看到上面的例子是一个很宽松的 URL。这意味大量的 URL patterns 都会被它匹配,因为它定义在 URL 的根,而不像 /profile// 这样。在这种情况下,如果我们想定义一个 /about/ 的URL,我们要把它定义在这个 username URL pattern 的前面:

from django.conf.urls import url
from boards import views

urlpatterns = [
    url(r'^$', views.home, name='home'),
    url(r'^about/$', views.about, name='about'),
    url(r'^(?P<username>[\w.@+-]+)/$', views.user_profile, name='user_profile'),
]

如果这个 "about" 页面定义在u srename URL pattern 后面,Django 将永远找不到它,因为 "about" 这个单词会先被usrname的正则表达式所匹配到,视图函数 user_profile 将会被执行而不是执行 about

此外,这有一些副作用。例如,从现在开始,我们要把 "about" 视为禁止使用的username,因为如果有用户将 "about" 作为他们的username,他们将永远不能看到他们的个人资料页面,而看到的about页面。

如果你想给用户个人主页设置一个很酷的主页的URL,那么避免冲突最简单的方法是添加一个前缀,例如:/u/vitorfs,或者像 Medium 一样使用 @ 作为前缀 /@vitorfs/。

这些 URL 路由的主要思想是当 URL 的一部分被当作某些资源(这些资源用来构成某个页面)的标识的时候就去创建一个动态页面。比如说,这个标识可以是一个整数的 ID 或者是一个字符串。

开始的时候,我们使用 Board ID 去创建 Topics列表的动态页面。让我们再来看一下我在 URLs 开头的部分给出的例子:

url(r'^boards/(?P<pk>\d+)/$', views.board_topics, name='board_topics')

正则表达式中的 \d+ 会匹配一个任意大小的整数值。这个整数值用来从数据库中取到 指定的 Board。现在注意我们这样写这个正则表达式 (?P<pk>\d+),这是告诉 Django 将捕获到的值放入名为 pk 的关键字参数中。

这时我们为它写的一个视图函数:

def board_topics(request, pk):
    # do something...

因为我们使用了 (?P<pk>\d+) 正则表达式,在 board_topics函数中,关键字参数必须命名为 pk

如果你想在视图函数使用任意名字的参数,那么可以这样定义:

url(r'^boards/(\d+)/$', views.board_topics, name='board_topics')

然后在视图函数可以这样定义:

def board_topics(request, board_id):
    # do something...

或者这样:

def board_topics(request, id):
    # do something...

名字无关紧要,但是使用命名参数是一个很好的做法,因为,当我们有个更大的URL去捕获多个 ID 和变量时,这会更便于我们阅读。

PK or ID? PK 表示主键(Primary key),这是访问模型的主键ID的简写方法,所有Django模型都有这个属性,更多的时候,使用pk属性和使用id是一样的,这是因为如果我们没有给model定义主键时,Django将自动创建一个 AutoField 类型的字段,名字叫做 id,它就是主键。
如果你给model定义了一个不同的主键,例如,假设 email 是你的主键,你就可以这样访问:obj.email 或者 obj.pk,二者是等价的。

使用 URLs API

现在到了写代码的时候了。我们来实现我在开头提到的主题列表页面

首先,编辑 urls.py, 添加新的 URL 路由

myproject/urls.py

from django.conf.urls import url
from django.contrib import admin

from boards import views

urlpatterns = [
    url(r'^$', views.home, name='home'),
    url(r'^boards/(?P<pk>\d+)/$', views.board_topics, name='board_topics'),
    url(r'^admin/', admin.site.urls),
]

现在创建视图函数 board_topics

boards/views.py

from django.shortcuts import render
from .models import Board

def home(request):
    # code suppressed for brevity

def board_topics(request, pk):
    board = Board.objects.get(pk=pk)
    return render(request, 'topics.html', {'board': board})

templates 目录中,创建一个名为 topics.html 的模板:

templates/topics.html

{% load static %}<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>{{ board.name }}</title>
    <link rel="stylesheet" href="{% static 'css/bootstrap.min.css' %}">
  </head>
  <body>
    <div class="container">
      <ol class="breadcrumb my-4">
        <li class="breadcrumb-item">Boards</li>
        <li class="breadcrumb-item active">{{ board.name }}</li>
      </ol>
    </div>
  </body>
</html>

注意:我们现在只是创建新的 HTML 模板。不用担心,在下一节中我会向你展示如何创建可重用模板。

现在在浏览器中打开 URL http://127.0.0.1:8000/boards/1/ ,结果应该是下面这个页面:

现在到了写一些测试的时候了!编辑 test.py,在文件底部添加下面的测试:

boards/tests.py

from django.core.urlresolvers import reverse
from django.urls import resolve
from django.test import TestCase
from .views import home, board_topics
from .models import Board

class HomeTests(TestCase):
    # ...

class BoardTopicsTests(TestCase):
    def setUp(self):
        Board.objects.create(name='Django', description='Django board.')

    def test_board_topics_view_success_status_code(self):
        url = reverse('board_topics', kwargs={'pk': 1})
        response = self.client.get(url)
        self.assertEquals(response.status_code, 200)

    def test_board_topics_view_not_found_status_code(self):
        url = reverse('board_topics', kwargs={'pk': 99})
        response = self.client.get(url)
        self.assertEquals(response.status_code, 404)

    def test_board_topics_url_resolves_board_topics_view(self):
        view = resolve('/boards/1/')
        self.assertEquals(view.func, board_topics)

这里需要注意几件事情。这次我们使用了 setUp 方法。在这个方法中,我们创建了一个 Board 实例来用于测试。我们必须这样做,因为 Django 的测试机制不会针对当前数据库跑你的测试。运行 Django 测试时会即时创建一个新的数据库,应用所有的model(模型)迁移 ,运行测试完成后会销毁这个用于测试的数据库。

因此在 setUp 方法中,我们准备了运行测试的环境,用来模拟场景。

  • test_board_topics_view_success_status_code 方法:测试 Django 是否对于现有的 Board 返回 status code(状态码) 200(成功)。
  • test_board_topics_view_not_found_status_code 方法:测试 Django 是否对于不存在于数据库的 Board 返回 status code 404(页面未找到)。
  • test_board_topics_url_resolves_board_topics_view 方法:测试 Django 是否使用了正确的视图函数去渲染 topics。

现在来运行一下测试:

python manage.py test

输出如下:

Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.E...
======================================================================
ERROR: test_board_topics_view_not_found_status_code (boards.tests.BoardTopicsTests)
----------------------------------------------------------------------
Traceback (most recent call last):
# ...
boards.models.DoesNotExist: Board matching query does not exist.

----------------------------------------------------------------------
Ran 5 tests in 0.250s

FAILED (errors=1)
Destroying test database for alias 'default'...

测试 test_board_topics_view_not_found_status_code 失败。我们可以在 Traceback 中看到返回了一个 exception(异常) “boards.models.DoesNotExist: Board matching query does not exist.”

DEBUG=False 的生产环境中,访问者会看到一个 500 Internal Server Error 的页面。但是这不是我们希望得到的。

我们想要一个 404 Page Not Found 的页面。让我们来重写我们的视图函数。

boards/views.py

from django.shortcuts import render
from django.http import Http404
from .models import Board

def home(request):
    # code suppressed for brevity

def board_topics(request, pk):
    try:
        board = Board.objects.get(pk=pk)
    except Board.DoesNotExist:
        raise Http404
    return render(request, 'topics.html', {'board': board})

重新测试一下:

python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.....
----------------------------------------------------------------------
Ran 5 tests in 0.031s

OK
Destroying test database for alias 'default'...

好极了!现在它按照预期工作。

这是 Django 在 DEBUG=False 的情况下显示的默认页面。稍后,我们可以自定义 404 页面去显示一些其他的东西。

这是一个常见的用法。事实上, Django 有一个快捷方式去得到一个对象,或者返回一个不存在的对象 404。

因此让我们再来重写一下 board_topics 函数:

from django.shortcuts import render, get_object_or_404
from .models import Board

def home(request):
    # code suppressed for brevity

def board_topics(request, pk):
    board = get_object_or_404(Board, pk=pk)
    return render(request, 'topics.html', {'board': board})

修改了代码,测试一下。

python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.....
----------------------------------------------------------------------
Ran 5 tests in 0.016s

OK
Destroying test database for alias 'default'...

没有破坏任何东西。我们可以继续我们的开发。

下一步是在屏幕上创建一个导航链接。主页应该有一个链接指引访问者去访问指定板块下面的主题列表页面。同样地,topics 页面也应当有一个返回主页的链接。

我们可以先为 HomeTests 类编写一些测试:

boards/test.py

class HomeTests(TestCase):
    def setUp(self):
        self.board = Board.objects.create(name='Django', description='Django board.')
        url = reverse('home')
        self.response = self.client.get(url)

    def test_home_view_status_code(self):
        self.assertEquals(self.response.status_code, 200)

    def test_home_url_resolves_home_view(self):
        view = resolve('/')
        self.assertEquals(view.func, home)

    def test_home_view_contains_link_to_topics_page(self):
        board_topics_url = reverse('board_topics', kwargs={'pk': self.board.pk})
        self.assertContains(self.response, 'href="{0}"'.format(board_topics_url))

注意到现在我们同样在 HomeTests 中添加了 setUp 方法。这是因为我们现在需要一个 Board 实例,并且我们将 urlresponse 移到了 setUp,所以我们能在新测试中重用相同的 response。

这里的新测试是 test_home_view_contains_link_to_topics_page。我们使用 assertContains 方法来测试 response 主体部分是否包含给定的文本。我们在测试中使用的文本是 a 标签的 href 部分。所以基本上我们是在测试 response 主体是否包含文本 href="/boards/1/"

让我们运行这个测试:

python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
....F.
======================================================================
FAIL: test_home_view_contains_link_to_topics_page (boards.tests.HomeTests)
----------------------------------------------------------------------
# ...

AssertionError: False is not true : Couldn't find 'href="/boards/1/"' in response

----------------------------------------------------------------------
Ran 6 tests in 0.034s

FAILED (failures=1)
Destroying test database for alias 'default'...

现在我们可以编写能通过这个测试的代码。

编写 home.html 模板:

templates/home.html

<!-- code suppressed for brevity -->
<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">0</td>
      <td class="align-middle">0</td>
      <td></td>
    </tr>
  {% endfor %}
</tbody>
<!-- code suppressed for brevity -->

我们只改动了这一行:

{{ board.name }}

变为:

<a href="{% url 'board_topics' board.pk %}">{{ board.name }}</a>

始终使用 {% url %} 模板标签去写应用的 URL。第一个参数是 URL 的名字(定义在 URLconf, 即 urls.py),然后你可以根据需求传递任意数量的参数。

如果是一个像主页这种简单的 URL, 那就是 {% url 'home' %}。 保存文件然后再运行一下测试:

python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
......
----------------------------------------------------------------------
Ran 6 tests in 0.031s

OK
Destroying test database for alias 'default'...

很棒!现在我们可以看到它在浏览器是什么样子。

现在轮到返回的链接了,我们可以先写测试:

boards/tests.py

class BoardTopicsTests(TestCase):
    # code suppressed for brevity...

    def test_board_topics_view_contains_link_back_to_homepage(self):
        board_topics_url = reverse('board_topics', kwargs={'pk': 1})
        response = self.client.get(board_topics_url)
        homepage_url = reverse('home')
        self.assertContains(response, 'href="{0}"'.format(homepage_url))

运行测试:

python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.F.....
======================================================================
FAIL: test_board_topics_view_contains_link_back_to_homepage (boards.tests.BoardTopicsTests)
----------------------------------------------------------------------
Traceback (most recent call last):
# ...

AssertionError: False is not true : Couldn't find 'href="/"' in response

----------------------------------------------------------------------
Ran 7 tests in 0.031s

FAILED (failures=1)
Destroying test database for alias 'default'...

更新主题列表模版:

templates/topics.html

{% load static %}<!DOCTYPE html>
<html>
  <head><!-- code suppressed for brevity --></head>
  <body>
    <div class="container">
      <ol class="breadcrumb my-4">
        <li class="breadcrumb-item"><a href="{% url 'home' %}">Boards</a></li>
        <li class="breadcrumb-item active">{{ board.name }}</li>
      </ol>
    </div>
  </body>
</html>

运行测试:

python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.......
----------------------------------------------------------------------
Ran 7 tests in 0.031s

OK
Destroying test database for alias 'default'...

就如我之前所说的, URL 路由是一个 web 应用程序的基本组成部分。有了这些知识,我们才能继续开发。下一步是完成 URL 的部分,你会看到一些使用 URL patterns 的总结。

实用URL模式列表

技巧部分是正则表达式。我准备了一个最常用的 URL patterns 的列表。当你需要一个特定的 URL 时你可以参考这个列表。

主键自增字段

正则表达式 (?P<pk>\d+)
举例 url(r'^questions/(?P<pk>\d+)/$', views.question, name='question')
有效 URL /questions/934/
捕获数据 {'pk': '934'}

Slug 字段

正则表达式 (?P<slug>[-\w]+)
举例 url(r'^posts/(?P<slug>[-\w]+)/$', views.post, name='post')
有效 URL /posts/hello-world/
捕获数据 {'slug': 'hello-world'}

有主键的 Slug 字段

正则表达式 (?P<slug>[-\w]+)-(?P<pk>\d+)
举例 url(r'^blog/(?P<slug>[-\w]+)-(?P<pk>\d+)/$', views.blog_post, name='blog_post')
有效 URL /blog/hello-world-159/
捕获数据 {'slug': 'hello-world', 'pk': '159'}

Django 用户名

正则表达式 (?P<username>[\w.@+-]+)
举例 url(r'^profile/(?P<username>[\w.@+-]+)/$', views.user_profile, name='user_profile')
有效 URL /profile/vitorfs/
捕获数据 {'username': 'vitorfs'}

Year

正则表达式 (?P<year>[0-9]{4})
举例 url(r'^articles/(?P<year>[0-9]{4})/$', views.year_archive, name='year')
有效 URL /articles/2016/
捕获数据 {'year': '2016'}

Year / Month

正则表达式 (?P<year>[0-9]{4})/(?P<month>[0-9]{2})
举例 url(r'^articles/(?P<year>[0-9]{4})/(?P<month>[0-9]{2})/$', views.month_archive, name='month')
有效 URL /articles/2016/01/
捕获数据 {'year': '2016', 'month': '01'}

你可以在这篇文章中看到更多关于正则表达式匹配的细节:List of Useful URL Patterns

原文:https://github.com/pythonzhichan/django-beginners-guide/blob/master/AdvancedConcepts.md

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