在本教程中,我们将深入探讨两个基本概念:URL和表单。 在此过程中,我们将探索许多其他概念,如创建可重用模板和安装第三方库。 我们还将编写大量的单元测试。
如果您从第一部分开始遵循本教程系列,对项目进行编码并逐步学习本教程,则可能需要在开始之前更新models.py :
板/ models.py
class Topic ( models . Model ): # other fields... # Add `auto_now_add=True` to the `last_updated` field last_updated = models . DateTimeField ( auto_now_add = True ) class Post ( models . Model ): # other fields... # Add `null=True` to the `updated_by` field updated_by = models . ForeignKey ( User , null = True , related_name = '+' )
现在运行virtualenv激活的命令:
python manage.py makemigrations python manage.py migrate
如果您在updated_by字段中已经有null=True在last_updated字段中已经有auto_now_add=True ,那么您可以放心地忽略上面的说明。
如果您更喜欢使用我的源代码作为起点,可以在GitHub上获取它。
可以在发布标记v0.2-lw下找到项目的当前状态。 以下链接将带您到正确的位置:
https://github.com/sibtc/django-beginners-guide/tree/v0.2-lw
发展将从这里开始。
网址
继续开发我们的应用程序,现在我们必须实现一个新页面来列出属于给定Board的所有主题。 回顾一下,您可以在下面看到我们在上一个教程中提到的线框:
图1:Boards项目线框列出了Django板中的所有主题。
我们将从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 ), ]
这一刻我们花点时间分析一下urlpatterns和url 。
URL调度程序和URLconf (URL配置)是Django应用程序的基本部分。 一开始,它看起来很混乱; 我记得第一次开始使用Django时遇到了困难。
事实上,现在Django开发人员正在制定一个简化路由语法的提案 。 但就目前而言,根据1.11版本,这就是我们所拥有的。 所以让我们试着了解它是如何工作的。
一个项目可以在应用程序之间分配许多urls.py. 但Django需要一个url.py作为起点。 这个特殊的urls.py称为root URLconf 。 它在settings.py文件中定义。
的myproject / settings.py
ROOT_URLCONF = 'myproject.urls'
它已经配置好了,因此您无需在此处进行任何更改。
当Django收到请求时,它开始在项目的URLconf中搜索匹配项。 它从urlpatterns变量的第一个条目开始,并针对每个url条目测试请求的URL。
如果Django找到匹配项,它会将请求传递给view函数 ,这是url的第二个参数。 urlpatterns的顺序urlpatterns重要,因为Django会在找到匹配后立即停止搜索。 现在,如果Django在URLconf中找不到匹配项,则会引发404异常,这是Page Not Found的错误代码。
这是url函数的解剖:
def url ( regex , view , kwargs = None , name = None ): # ...
- regex :用于匹配字符串中的URL模式的正则表达式。 请注意,这些正则表达式不会搜索GET或POST参数。 在http://127.0.0.1:8000/boards/?page=2的请求中,只处理/ boards / 。
- view :用于处理匹配URL的用户请求的视图函数。 它还接受django.conf.urls.include函数的返回,该函数用于引用外部urls.py文件。 例如,您可以使用它来定义一组特定于应用程序的URL,并使用前缀将其包含在根URLconf中。 我们稍后会详细探讨这个概念。
- kwargs :传递给目标视图的任意关键字参数。 它通常用于对可重用视图进行一些简单的自定义。 我们不经常使用它。
- name :给定URL的唯一标识符。 这是一个非常重要的功能。 始终记得为您的网址命名。 有了这个,您只需更改正则表达式即可更改整个项目中的特定URL。 因此,永远不要在视图或模板中对URL进行硬编码,并始终按名称引用URL非常重要。
基本网址
基本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结构:
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' })
高级网址
通过利用正则表达式匹配某些类型的数据并创建动态URL,可以实现URL路由的更高级用法。
例如,要创建配置文件页面,就像许多服务一样,例如github.com/vitorfs或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模式,因为它是在URL的根目录中定义的,没有像/ profile / <username> /这样的前缀。 在这种情况下,如果我们想要定义名为/ about /的URL,我们将在用户名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'^(?P<username>[ \ w.@+-]+)/$' , views . user_profile , name = 'user_profile' ), ]
如果在用户名URL模式之后定义了“about”页面,Django将永远找不到它,因为单词“about”将匹配用户名正则表达式,并且将处理视图user_profile而不是about视图函数。
这有一些副作用。 例如,从现在开始,我们必须将“约”视为禁用用户名,因为如果用户选择“约”作为其用户名,则此人永远不会看到他们的个人资料页面。
旁注:如果要为用户配置文件设计很酷的URL,避免URL冲突的最简单的解决方案是添加像/ u / vitorfs /这样的前缀,或像Medium do / @ vitorfs / ,其中“@”是前缀。
如果您根本不需要前缀,请考虑使用以下禁用名称列表: github.com/shouldbee/reserved-usernames 。 或者另一个例子是我在学习Django时开发的应用程序; 我当时创建了我的列表: github.com/vitorfs/parsifal/ 。
那些碰撞很常见。 以GitHub为例; 他们有这个URL列出您当前正在观看的所有存储库: github.com/watching 。 有人在GitHub上注册了一个名为“观看”的用户名,因此此人无法看到他的个人资料页面。 我们可以通过尝试此URL来查看具有此用户名的用户: github.com/watching/repositories应该列出用户的存储库,例如我的github.com/vitorfs/repositories 。
这种URL路由的整个想法是创建动态页面,其中URL的一部分将用作特定资源的标识符,用于组成页面。 例如,该标识符可以是整数ID或字符串。
最初,我们将使用Board ID为主题创建动态页面。 让我们再次阅读我在URL部分开头给出的示例:
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...
这个名字无关紧要。 但是使用命名参数是一个很好的做法,因为当我们开始编写捕获多个ID和变量的更大的URL时,它将更容易阅读。
旁注: PK还是ID?
PK代表主键 。 这是访问模型主键的快捷方式。 所有Django模型都具有此属性。
对于大多数情况,使用pk属性与id相同。 那是因为如果我们没有为模型定义主键,Django将自动创建一个名为id的AutoField ,它将是它的主键。
例如,如果您为模型定义了不同的主键,请假设字段email是您的主键。 要访问它,您可以使用obj.email或obj.pk
使用URL API
是时候写一些代码了。 让我们实现我在URL部分开头提到的主题列表页面(参见图1 )。
首先,编辑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 :
板/ 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的新模板:
模板/ 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模板。 不用担心,在下一节中,我将向您展示如何创建可重用模板。
现在在Web浏览器中检查URL http://127.0.0.1:8000/boards/1/ 。 结果应该是以下页面:
是时候写一些测试了! 编辑tests.py文件并在文件底部添加以下测试:
板/ 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方法。 在setup方法中,我们创建了一个在测试中使用的Board实例。 我们必须这样做,因为Django测试套件不会针对当前数据库运行测试。 要运行测试,Django会动态创建一个新数据库,应用所有模型迁移,运行测试,并在完成后销毁测试数据库。
所以在setUp方法中,我们准备环境来运行测试,以便模拟场景。
- test_board_topics_view_success_status_code方法:正在测试Django是否返回现有Board的状态代码200(成功)。
- test_board_topics_view_not_found_status_code方法:正在测试Django是否为数据库中不存在的Board返回状态代码404(找不到页面)。
- test_board_topics_url_resolves_board_topics_view方法:正在测试Django是否使用正确的视图函数来呈现主题。
现在是时候运行测试了:
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.093s FAILED (errors=1) Destroying test database for alias 'default'...
测试test_board_topics_view_not_found_status_code失败。 我们可以在Traceback中看到它返回了一个异常“boards.models.DoesNotExist:Board匹配查询不存在。”
在使用DEBUG=False生产中,访问者将看到500 Internal Server Error页面。 但这不是我们想要的行为。
我们想要显示404 Page Not Found 。 让我们重构一下我们的观点:
板/ 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.042s 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.052s OK Destroying test database for alias 'default'...
没有破坏任何东西。 我们可以继续发展。
现在,下一步是在屏幕中创建导航链接。 主页应该有一个链接,将访问者带到给定董事会的主题页面。 同样,主题页面应该有一个返回主页的链接。
我们可以从为HomeTests类编写一些测试开始:
板/ tests.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实例,并且我们还将url和响应移动到setUp ,因此我们可以在新测试中重用相同的响应。
这里的新测试是test_home_view_contains_link_to_topics_page 。 这里我们使用assertContains方法来测试响应主体是否包含给定的文本。 我们在测试中使用的文本是标记的href部分。 所以基本上我们测试响应体是否有文本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模板:
模板/ 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 % }模板标记用于组成应用程序URL。 第一个参数是URL的名称 (在URLconf中定义,即urls.py ),然后您可以根据需要传递任意数量的参数。
如果它是一个简单的URL,如主页,它将只是{ % url ‘home’ % } { % url ‘home’ % } { % url ‘home’ % } 。
保存文件并再次运行测试:
python manage.py test
Creating test database for alias 'default'... System check identified no issues (0 silenced). ...... ---------------------------------------------------------------------- Ran 6 tests in 0.037s OK Destroying test database for alias 'default'...
好! 现在我们可以检查它在Web浏览器中的外观:
现在链接回来了。 我们可以先写测试:
板/ 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.054s FAILED (failures=1) Destroying test database for alias 'default'...
更新电路板主题模板:
模板/ 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.061s OK Destroying test database for alias 'default'...
正如我之前提到的,URL路由是Web应用程序的基本组成部分。 有了这些知识,我们应该能够继续开发。 接下来,要完成有关URL的部分,您将找到有用的URL模式的摘要。
有用的URL模式列表
技巧部分是正则表达式 。 所以我准备了一个最常用的URL模式列表。 当您需要特定URL时,您始终可以参考此列表。
主键自动字段 | |
---|---|
正则表达式 | (?P<pk>\d+) |
例 | url(r’^questions/(?P<pk>\d+)/$’, views.question, name=’question’) |
有效的URL | /questions/934/ |
捕获 | {‘pk’: ‘934’} |
Slug Field | |
---|---|
正则表达式 | (?P<slug>[-\w]+) |
例 | url(r’^posts/(?P<slug>[-\w]+)/$’, views.post, name=’post’) |
有效的URL | /posts/hello-world/ |
捕获 | {‘slug’: ‘hello-world’} |
带主键的弹头场 | |
---|---|
正则表达式 | (?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’} |
年 | |
---|---|
正则表达式 | (?P<year>[0-9]{4}) |
例 | url(r’^articles/(?P<year>[0-9]{4})/$’, views.year_archive, name=’year’) |
有效的URL | /articles/2016/ |
捕获 | {‘year’: ‘2016’} |
年/月 | |
---|---|
正则表达式 | (?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’} |
您可以在此文章中找到有关这些模式的更多详细信息: 有用的URL模式列表 。
可重复使用的模板
到目前为止,我们一直在复制和粘贴HTML,重复HTML文档的几个部分,从长远来看这是不可持续的。 这也是一种不好的做法。
在本节中,我们将重构HTML模板,创建母版页并仅为每个模板添加唯一部分。
在templates文件夹中创建一个名为base.html的新文件:
模板/ base.html文件
{% load static %}<!DOCTYPE html> <html> <head> <meta charset= "utf-8" > <title> {% block title %} Django Boards {% endblock %} </title> <link rel= "stylesheet" href= " {% static 'css/bootstrap.min.css' %} " > </head> <body> <div class= "container" > <ol class= "breadcrumb my-4" > {% block breadcrumb %} {% endblock %} </ol> {% block content %} {% endblock %} </div> </body> </html>
这将成为我们的母版页。 我们创建的每个模板都将扩展此特殊模板。 现在观察我们介绍了{ % block % } { % block % } { % block % }标签。 它用于在模板中保留一个空间,“子”模板(扩展主页面)可以在该空间中插入代码和HTML。
在{ % block title % }的情况下 { % block title % } { % block title % }我们还设置了一个默认值,即“Django Boards。”如果我们没有为{ % block title % }设置值,它将被使用 { % block title % } 子模板中的{ % block title % } 。
现在让我们重构我们的两个模板: home.html和topics.html 。
模板/ 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" > 0 </td> <td class= "align-middle" > 0 </td> <td></td> </tr> {% endfor %} </tbody> </table> {% endblock %}
home.html模板中的第一行是{ % extends ‘base.html’ % } { % extends ‘base.html’ % } { % extends ‘base.html’ % } 。 此标记告诉Django将base.html模板用作母版页。 之后,我们使用块来放置页面的唯一内容。
模板/ topics.html
{% extends 'base.html' %} {% block title %} {{ board.name }} - {{ block.super }} {% endblock %} {% block breadcrumb %} <li class= "breadcrumb-item" ><a href= " {% url 'home' %} " > Boards </a></li> <li class= "breadcrumb-item active" > {{ board.name }} </li> {% endblock %} {% block content %} <!-- just leaving it empty for now. we will add core here soon. --> {% endblock %}
在topics.html模板中,我们正在更改{ % block title % } { % block title % } { % block title % }默认值。 请注意,我们可以通过调用{ { block.super } }来重用块的默认值 { { block.super } } { { block.super } } 。 所以我们在这里玩网站标题,我们在base.html中定义为“Django Boards”。因此对于“Python”主板页面,标题将是“Python – Django Boards”,用于“随机”板标题将是“Random – Django Boards”。
现在让我们运行测试,看看我们没有破坏任何东西:
python manage.py test
Creating test database for alias 'default'... System check identified no issues (0 silenced). ....... ---------------------------------------------------------------------- Ran 7 tests in 0.067s OK Destroying test database for alias 'default'...
大! 一切都很好看。
现在我们已经有了base.html模板,我们可以轻松添加一个带有菜单的顶栏:
模板/ base.html文件
{% load static %}<!DOCTYPE html> <html> <head> <meta charset= "utf-8" > <title> {% block title %} Django Boards {% endblock %} </title> <link rel= "stylesheet" href= " {% static 'css/bootstrap.min.css' %} " > </head> <body> <nav class= "navbar navbar-expand-lg navbar-dark bg-dark" > <div class= "container" > <a class= "navbar-brand" href= " {% url 'home' %} " > Django Boards </a> </div> </nav> <div class= "container" > <ol class= "breadcrumb my-4" > {% block breadcrumb %} {% endblock %} </ol> {% block content %} {% endblock %} </div> </body> </html>
我使用的HTML是Bootstrap 4 Navbar Component的一部分 。
我想添加的一个很好的方法是更改页面的“logo”(. .navbar-brand )中的字体。
转到fonts.google.com ,键入“Django Boards”或您为项目指定的任何名称,然后单击“ 应用于所有字体” 。 浏览一下,找一个你喜欢的。
在base.html模板中添加字体:
{% load static %}<!DOCTYPE html> <html> <head> <meta charset= "utf-8" > <title> {% block title %} Django Boards {% endblock %} </title> <link href= "https://fonts.googleapis.com/css?family=Peralta" rel= "stylesheet" > <link rel= "stylesheet" href= " {% static 'css/bootstrap.min.css' %} " > <link rel= "stylesheet" href= " {% static 'css/app.css' %} " > </head> <body> <!-- code suppressed for brevity --> </body> </html>
现在在static / css文件夹中创建一个名为app.css的新CSS文件:
静电/ CSS / app.css
.navbar-brand { font-family : 'Peralta' , cursive ; }
形式
表单用于处理用户输入。 这是任何Web应用程序或网站中非常常见的任务。 标准的方法是通过HTML表单,用户输入一些数据,将其提交给服务器,然后服务器使用它做一些事情。
表单处理是一项相当复杂的任务,因为它涉及与应用程序的多个层进行交互。 还有许多问题需要处理。 例如,提交给服务器的所有数据都是字符串格式,因此在使用它之前我们必须将其转换为适当的数据类型(整数,浮点数,日期等)。 我们必须验证有关应用程序业务逻辑的数据。 我们还必须正确清理和清理数据,以避免SQL注入和XSS攻击等安全问题。
好消息是,Django Forms API使整个过程变得更加容易,自动完成了大部分工作。 此外,最终结果是比大多数程序员自己能够实现的代码更安全的代码。 因此,无论HTML表单多么简单,始终使用表单API。
如何不实施表格
起初,我想过直接跳转到表单API。 但我认为我们花一些时间试图理解表单处理的基本细节是个好主意。 否则,它最终看起来像魔术,这是一件坏事,因为当出现问题时,你不知道在哪里寻找问题。
通过对一些编程概念的深入理解,我们可以更好地控制局面。 控制是很重要的,因为它让我们更自信地编写代码。 在我们确切知道发生了什么的那一刻,实现可预测行为的代码要容易得多。 调试和查找错误也更容易,因为您知道在哪里查看。
无论如何,让我们从实施以下表格开始:
这是我们在上一个教程中提到的线框之一。 我现在意识到这可能是一个不好的例子,因为这个特定的形式涉及处理两个不同模型的数据: 主题 (主题)和发布 (消息)。
到目前为止,我们还没有讨论过另一个重要方面,即用户身份验证。 我们只应该为经过身份验证的用户显示此屏幕。 通过这种方式,我们可以判断谁创建了主题或帖子 。
因此,现在让我们抽象一些细节,重点介绍如何在数据库中保存用户输入。
首先,让我们创建一个名为new_topic的新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'^boards/(?P<pk> \ d+)/new/$' , views . new_topic , name = 'new_topic' ), url ( r'^admin/' , admin . site . urls ), ]
我们构建URL的方式将帮助我们识别正确的Board 。
现在让我们创建new_topic视图函数:
板/ views.py
from django.shortcuts import render , get_object_or_404 from .models import Board def new_topic ( request , pk ): board = get_object_or_404 ( Board , pk = pk ) return render ( request , 'new_topic.html' , { 'board' : board })
目前, new_topic视图函数看起来与board_topics完全相同 。 这是故意的,让我们一步一步。
现在我们只需要一个名为new_topic.html的模板来查看一些有效的代码:
模板/ new_topic.html
{% extends 'base.html' %} {% block title %} Start a New Topic {% endblock %} {% block breadcrumb %} <li class= "breadcrumb-item" ><a href= " {% url 'home' %} " > Boards </a></li> <li class= "breadcrumb-item" ><a href= " {% url 'board_topics' board.pk %} " > {{ board.name }} </a></li> <li class= "breadcrumb-item active" > New topic </li> {% endblock %} {% block content %} {% endblock %}
现在我们只需要面包屑来确保导航。 注意我们将URL包含在board_topics视图中。
打开URL http://127.0.0.1:8000/boards/1/new/ 。 目前的结果是以下页面:
我们还没有实现到达这个新页面的方法,但如果我们将URL更改为http://127.0.0.1:8000/boards/2/new/ ,它应该将我们带到Python Board :
注意:
如果您没有按照上一个教程中的步骤操作,结果可能会有所不同。 在我的例子中,我在数据库中有三个Board实例,Django = 1,Python = 2,Random = 3.这些数字是数据库中的ID,用于从URL中识别正确的资源。
我们已经可以添加一些测试:
板/ tests.py
from django.core.urlresolvers import reverse from django.urls import resolve from django.test import TestCase from .views import home , board_topics , new_topic from .models import Board class HomeTests ( TestCase ): # ... class BoardTopicsTests ( TestCase ): # ... class NewTopicTests ( TestCase ): def setUp ( self ): Board . objects . create ( name = 'Django' , description = 'Django board.' ) def test_new_topic_view_success_status_code ( self ): url = reverse ( 'new_topic' , kwargs = { 'pk' : 1 }) response = self . client . get ( url ) self . assertEquals ( response . status_code , 200 ) def test_new_topic_view_not_found_status_code ( self ): url = reverse ( 'new_topic' , kwargs = { 'pk' : 99 }) response = self . client . get ( url ) self . assertEquals ( response . status_code , 404 ) def test_new_topic_url_resolves_new_topic_view ( self ): view = resolve ( '/boards/1/new/' ) self . assertEquals ( view . func , new_topic ) def test_new_topic_view_contains_link_back_to_board_topics_view ( self ): new_topic_url = reverse ( 'new_topic' , kwargs = { 'pk' : 1 }) board_topics_url = reverse ( 'board_topics' , kwargs = { 'pk' : 1 }) response = self . client . get ( new_topic_url ) self . assertContains ( response , 'href="{0}"' . format ( board_topics_url ))
我们新类NewTopicTests测试的快速摘要:
- setUp :创建在测试期间使用的Board实例
- test_new_topic_view_success_status_code :检查对视图的请求是否成功
- test_new_topic_view_not_found_status_code :当Board不存在时,检查视图是否引发404错误
- test_new_topic_url_resolves_new_topic_view :检查是否正在使用正确的视图
- test_new_topic_view_contains_link_back_to_board_topics_view:确保导航回主题列表
运行测试:
python manage.py test
Creating test database for alias 'default'... System check identified no issues (0 silenced). ........... ---------------------------------------------------------------------- Ran 11 tests in 0.076s OK Destroying test database for alias 'default'...
很好,现在是时候开始创建表单了。
模板/ new_topic.html
{% extends 'base.html' %} {% block title %} Start a New Topic {% endblock %} {% block breadcrumb %} <li class= "breadcrumb-item" ><a href= " {% url 'home' %} " > Boards </a></li> <li class= "breadcrumb-item" ><a href= " {% url 'board_topics' board.pk %} " > {{ board.name }} </a></li> <li class= "breadcrumb-item active" > New topic </li> {% endblock %} {% block content %} <form method= "post" > {% csrf_token %} <div class= "form-group" > <label for= "id_subject" > Subject </label> <input type= "text" class= "form-control" id= "id_subject" name= "subject" > </div> <div class= "form-group" > <label for= "id_message" > Message </label> <textarea class= "form-control" id= "id_message" name= "message" rows= "5" ></textarea> </div> <button type= "submit" class= "btn btn-success" > Post </button> </form> {% endblock %}
这是使用Bootstrap 4提供的CSS类手工创建的原始HTML表单。它看起来像这样:
在<form>标签中,我们必须定义method属性。这将指示浏览器我们希望如何与服务器通信。HTTP规范定义了几种请求方法(动词)。但在大多数情况下,我们只会使用GET和POST请求类型。
GET可能是最常见的请求类型。它用于从服务器检索数据。每次单击链接或直接在浏览器中键入URL时,都会创建GET请求。
当我们想要更改服务器上的数据时使用POST。因此,一般来说,每次我们向服务器发送数据时都会导致资源状态发生变化,我们应该始终通过POST请求发送数据。
Django 使用CSRF令牌(跨站点请求伪造令牌)保护所有POST请求。这是避免外部站点或应用程序向我们的应用程序提交数据的安全措施。每次应用程序收到POST时,它将首先查找CSRF令牌。如果请求没有令牌,或令牌无效,它将丢弃发布的数据。
csrf_token模板标记的结果:
{% csrf_token %}
是与其他表单数据一起提交的隐藏字段:
<input type= "hidden" name= "csrfmiddlewaretoken" value= "jG2o6aWj65YGaqzCpl0TYTg5jn6SctjzRZ9KmluifVx0IVaxlwh97YarZKs54Y32" >
另外,我们必须设置HTML输入的名称。该名称将用于检索服务器端的数据。
<input type= "text" class= "form-control" id= "id_subject" name= "subject" > <textarea class= "form-control" id= "id_message" name= "message" rows= "5" ></textarea>
以下是我们检索数据的方法:
subject = request . POST [ 'subject' ] message = request . POST [ 'message' ]
因此,从HTML中获取数据并启动新主题的视图的简单实现可以这样写:
from django.contrib.auth.models import User from django.shortcuts import render , redirect , get_object_or_404 from .models import Board , Topic , Post def new_topic ( request , pk ): board = get_object_or_404 ( Board , pk = pk ) if request . method == 'POST' : subject = request . POST [ 'subject' ] message = request . POST [ 'message' ] user = User . objects . first () # TODO: get the currently logged in user topic = Topic . objects . create ( subject = subject , board = board , starter = user ) post = Post . objects . create ( message = message , topic = topic , created_by = user ) return redirect ( 'board_topics' , pk = board . pk ) # TODO: redirect to the created topic page return render ( request , 'new_topic.html' , { 'board' : board })
此视图仅考虑快乐路径,即接收数据并将其保存到数据库中。但是有一些缺失的部分。我们没有验证数据。用户可以提交空表单或大于255个字符的主题。
到目前为止,我们正在对用户字段进行硬编码,因为我们还没有实现身份验证。但是有一种简单的方法可以识别登录用户。我们将在下一个教程中介绍该部分。此外,我们还没有实现视图,我们将列出主题中的所有帖子,因此一旦成功,我们将用户重定向到列出所有电路板主题的页面。
单击“ 发布”按钮提交表单:
它看起来很有效。但是我们还没有实现主题列表,所以这里没什么可看的。让我们编辑templates / topics.html文件以进行正确的列表:
模板/ topics.html
{% extends 'base.html' %} {% block title %} {{ board.name }} - {{ block.super }} {% endblock %} {% block breadcrumb %} <li class= "breadcrumb-item" ><a href= " {% url 'home' %} " > Boards </a></li> <li class= "breadcrumb-item active" > {{ board.name }} </li> {% endblock %} {% block content %} <table class= "table" > <thead class= "thead-inverse" > <tr> <th> Topic </th> <th> Starter </th> <th> Replies </th> <th> Views </th> <th> Last Update </th> </tr> </thead> <tbody> {% for topic in board.topics.all %} <tr> <td> {{ topic.subject }} </td> <td> {{ topic.starter.username }} </td> <td> 0 </td> <td> 0 </td> <td> {{ topic.last_updated }} </td> </tr> {% endfor %} </tbody> </table> {% endblock %}
是的!我们创建的主题就在这里。
这里有两个新概念:
我们首次使用Board模型中的topic属性。该主题酒店被Django的使用反向关系自动创建。在前面的步骤中,我们创建了一个Topic实例:
def new_topic ( request , pk ): board = get_object_or_404 ( Board , pk = pk ) # ... topic = Topic . objects . create ( subject = subject , board = board , starter = user )
在该行中board=board,我们在Topic模型中设置了board字段,这是一个。有了它,现在我们的Board实例知道它有一个与之关联的Topic实例。ForeignKey(Board)
我们之所以使用board.topics.all而不仅仅是board.topics因为board.topics是一个相关管理器,它与模型管理器非常相似,通常在board.objects属性上可用。因此,要返回与给定板相关的所有主题,我们必须运行board.topics.all()。要过滤一些数据,我们可以做到board.topics.filter(subject__contains=’Hello’)。
另一个需要注意的重要事项是,在Python代码中,我们必须使用括号:board.topics.all(),因为它all()是一个方法。使用Django模板语言编写代码时,在HTML模板文件中,我们不使用括号,所以它只是board.topics.all。
第二件事是我们正在利用ForeignKey:
{{ topic.starter.username }}
只需使用点创建一个穿过属性的路径。我们几乎可以访问User模型的任何属性。如果我们想要用户的电子邮件,我们可以使用topic.starter.email。
由于我们已经在修改topics.html模板,因此我们创建一个按钮,将我们带到新主题屏幕:
模板/ topics.html
{% block content %} <div class= "mb-4" > <a href= " {% url 'new_topic' board.pk %} " class= "btn btn-primary" > New topic </a> </div> <table class= "table" > <!-- code suppressed for brevity --> </table> {% endblock %}
我们可以包含一个测试,以确保用户可以从此页面访问新主题视图:
板/ tests.py
class BoardTopicsTests ( TestCase ): # ... def test_board_topics_view_contains_navigation_links ( self ): board_topics_url = reverse ( 'board_topics' , kwargs = { 'pk' : 1 }) homepage_url = reverse ( 'home' ) new_topic_url = reverse ( 'new_topic' , kwargs = { 'pk' : 1 }) response = self . client . get ( board_topics_url ) self . assertContains ( response , 'href="{0}"' . format ( homepage_url )) self . assertContains ( response , 'href="{0}"' . format ( new_topic_url ))
基本上我在这里重命名了旧的test_board_topics_view_contains_link_back_to_homepage方法并添加了一个额外的方法assertContains。此测试现在负责确保我们的视图包含所需的导航链接。
测试表单视图
在我们以Django方式编写前一个表单示例之前,让我们为表单处理编写一些测试:
板/ tests.py
''' new imports below ''' from django.contrib.auth.models import User from .views import new_topic from .models import Board , Topic , Post class NewTopicTests ( TestCase ): def setUp ( self ): Board . objects . create ( name = 'Django' , description = 'Django board.' ) User . objects . create_user ( username = 'john' , email = 'john@doe.com' , password = '123' ) # <- included this line here # ... def test_csrf ( self ): url = reverse ( 'new_topic' , kwargs = { 'pk' : 1 }) response = self . client . get ( url ) self . assertContains ( response , 'csrfmiddlewaretoken' ) def test_new_topic_valid_post_data ( self ): url = reverse ( 'new_topic' , kwargs = { 'pk' : 1 }) data = { 'subject' : 'Test title' , 'message' : 'Lorem ipsum dolor sit amet' } response = self . client . post ( url , data ) self . assertTrue ( Topic . objects . exists ()) self . assertTrue ( Post . objects . exists ()) def test_new_topic_invalid_post_data ( self ): ''' Invalid post data should not redirect The expected behavior is to show the form again with validation errors ''' url = reverse ( 'new_topic' , kwargs = { 'pk' : 1 }) response = self . client . post ( url , {}) self . assertEquals ( response . status_code , 200 ) def test_new_topic_invalid_post_data_empty_fields ( self ): ''' Invalid post data should not redirect The expected behavior is to show the form again with validation errors ''' url = reverse ( 'new_topic' , kwargs = { 'pk' : 1 }) data = { 'subject' : '' , 'message' : '' } response = self . client . post ( url , data ) self . assertEquals ( response . status_code , 200 ) self . assertFalse ( Topic . objects . exists ()) self . assertFalse ( Post . objects . exists ())
首先,tests.py文件已经开始变大。我们将很快改进它,将测试分成几个文件。但就目前而言,让我们继续努力吧。
- setUp:包含User.objects.create_user用于创建要在测试中使用的User实例
- test_csrf:由于CSRF令牌是处理POST请求的基本部分,我们必须确保我们的HTML包含令牌。
- test_new_topic_valid_post_data:发送有效的数据组合,并检查视图是否创建了Topic实例和Post实例。
- test_new_topic_invalid_post_data:这里我们发送一个空字典来检查应用程序的行为方式。
- test_new_topic_invalid_post_data_empty_fields:类似于之前的测试,但这次我们正在发送一些数据。该应用程序应该验证并拒绝空主题和消息。
让我们运行测试:
python manage.py test
Creating test database for alias 'default'... System check identified no issues (0 silenced). ........EF..... ====================================================================== ERROR: test_new_topic_invalid_post_data (boards.tests.NewTopicTests) ---------------------------------------------------------------------- Traceback (most recent call last): ... django.utils.datastructures.MultiValueDictKeyError: "'subject'" ====================================================================== FAIL: test_new_topic_invalid_post_data_empty_fields (boards.tests.NewTopicTests) ---------------------------------------------------------------------- Traceback (most recent call last): File "/Users/vitorfs/Development/myproject/django-beginners-guide/boards/tests.py", line 115, in test_new_topic_invalid_post_data_empty_fields self.assertEquals(response.status_code, 200) AssertionError: 302 != 200 ---------------------------------------------------------------------- Ran 15 tests in 0.512s FAILED (failures=1, errors=1) Destroying test database for alias 'default'...
我们有一个失败的测试和一个错误。两者都与无效的用户输入有关。不要试图用当前的实现来修复它,让我们使用Django Forms API来传递这些测试。
创造形式正确的方式
因此,自从我们开始使用Forms以来,我们走了很长的路。最后,是时候使用Forms API了。
Forms API在模块中可用django.forms。Django使用两种形式:forms.Form和forms.ModelForm。该Form班是一个通用的形式实现。我们可以使用它来处理与我们的应用程序中的模型没有直接关联的数据。A ModelForm是一个子类Form,它与模型类相关联。
让我们在boards ‘文件夹中创建一个名为forms.py的新文件:
板/ forms.py
from django import forms from .models import Topic class NewTopicForm ( forms . ModelForm ): message = forms . CharField ( widget = forms . Textarea (), max_length = 4000 ) class Meta : model = Topic fields = [ 'subject' , 'message' ]
这是我们的第一个表格。它ModelForm与Topic模型相关联。将subject在fields内部列表元类是指subject现场的主题类。现在观察我们正在定义一个名为的额外字段message。这是指我们要保存的帖子中的消息。
现在我们必须重构我们的views.py:
板/ views.py
from django.contrib.auth.models import User from django.shortcuts import render , redirect , get_object_or_404 from .forms import NewTopicForm from .models import Board , Topic , Post def new_topic ( request , pk ): board = get_object_or_404 ( Board , pk = pk ) user = User . objects . first () # TODO: get the currently logged in user if request . method == 'POST' : form = NewTopicForm ( request . POST ) if form . is_valid (): topic = form . save ( commit = False ) topic . board = board topic . starter = user topic . save () post = Post . objects . create ( message = form . cleaned_data . get ( 'message' ), topic = topic , created_by = user ) 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 })
这就是我们在视图中使用表单的方式。让我删除额外的噪音,以便我们可以专注于表单处理的核心:
if request . method == 'POST' : form = NewTopicForm ( request . POST ) if form . is_valid (): topic = form . save () return redirect ( 'board_topics' , pk = board . pk ) else : form = NewTopicForm () return render ( request , 'new_topic.html' , { 'form' : form })
首先,我们检查请求是POST还是GET。如果请求来自POST,则表示用户正在向服务器提交一些数据。因此,我们实例化一个将POST数据传递给表单的表单实例:form = NewTopicForm(request.POST)。
然后,我们要求Django验证数据,如果我们可以将其保存在数据库中,请检查表单是否有效:if form.is_valid():。如果表单有效,我们将继续使用数据保存在数据库中form.save()。该save()方法返回保存到数据库中的Model实例。因此,由于这是一个Topic表单,它将返回创建的主题:topic = form.save()。之后,常见的路径是将用户重定向到其他位置,以避免用户通过按F5重新提交表单并保持应用程序的流程。
现在,如果数据无效,Django将向表单添加错误列表。在那之后,视图什么都不做,并在最后一个语句中返回:return render(request, ‘new_topic.html’, {‘form’: form})。这意味着我们必须更新new_topic.html才能正确显示错误。
如果请求是GET,我们只需使用初始化一个新的空表单form = NewTopicForm()。
让我们运行测试,看看一切如何:
python manage.py test
Creating test database for alias 'default'... System check identified no issues (0 silenced). ............... ---------------------------------------------------------------------- Ran 15 tests in 0.522s OK Destroying test database for alias 'default'...
我们甚至修复了最后两个测试。
Django Forms API不仅仅是处理和验证数据。它还为我们生成HTML。
让我们更新new_topic.html模板以完全使用Django Forms API:
模板/ new_topic.html
{% extends 'base.html' %} {% block title %} Start a New Topic {% endblock %} {% block breadcrumb %} <li class= "breadcrumb-item" ><a href= " {% url 'home' %} " > Boards </a></li> <li class= "breadcrumb-item" ><a href= " {% url 'board_topics' board.pk %} " > {{ board.name }} </a></li> <li class= "breadcrumb-item active" > New topic </li> {% endblock %} {% block content %} <form method= "post" > {% csrf_token %} {{ form.as_p }} <button type= "submit" class= "btn btn-success" > Post </button> </form> {% endblock %}
将form有三种渲染选项:form.as_table,form.as_ul,和form.as_p。这是渲染表单所有字段的快捷方法。顾名思义,as_table使用表格标签来格式化输入,as_ul创建HTML输入列表等。
让我们看看它的样子:
好吧,我们之前的表格看起来更好,对吧?我们马上就要解决它。
它现在看起来很破碎,但相信我; 现在背后有很多东西。它非常强大。例如,如果我们的表单有50个字段,我们可以通过键入来渲染所有字段。{ { form.as_p } }
而且,使用Forms API,Django将验证数据并向每个字段添加错误消息。我们试着提交一份空表格:
注意:
如果您看到类似这样的内容:当您提交表单时,那不是Django。这是您的浏览器进行预验证。要禁用它,请将该novalidate属性添加到表单标记:<form method=”post” novalidate>
你可以留着; 它没有问题。这只是因为我们的表单现在非常简单,我们没有太多的数据验证。
另一个需要注意的重要事项是:没有“客户端验证”这样的事情。JavaScript验证或浏览器验证仅用于可用性目的。并且还减少了对服务器的请求数量。数据验证应始终在服务器端完成,我们可以完全控制数据。
它还处理帮助文本,可以在Form类或Model类中定义:
板/ forms.py
from django import forms from .models import Topic class NewTopicForm ( forms . ModelForm ): message = forms . CharField ( widget = forms . Textarea (), max_length = 4000 , help_text = 'The max length of the text is 4000.' ) class Meta : model = Topic fields = [ 'subject' , 'message' ]
我们还可以为表单字段设置额外的属性:
板/ forms.py
from django import forms from .models import Topic class NewTopicForm ( forms . ModelForm ): message = forms . CharField ( widget = forms . Textarea ( attrs = { 'rows' : 5 , 'placeholder' : 'What is on your mind?' } ), max_length = 4000 , help_text = 'The max length of the text is 4000.' ) class Meta : model = Topic fields = [ 'subject' , 'message' ]
渲染Bootstrap表单
好吧,让我们再把事情搞得一团糟。
使用Bootstrap或任何其他前端库时,我喜欢使用名为django-widget-tweaks的Django包。它使我们能够更好地控制渲染过程,保留默认值并在其上添加额外的自定义。
让我们从安装它开始:
pip install django-widget-tweaks
现在将其添加到INSTALLED_APPS:
的myproject / settings.py
INSTALLED_APPS = [ 'django.contrib.admin' , 'django.contrib.auth' , 'django.contrib.contenttypes' , 'django.contrib.sessions' , 'django.contrib.messages' , 'django.contrib.staticfiles' , 'widget_tweaks' , 'boards' , ]
现在让我们使用它:
模板/ new_topic.html
{% extends 'base.html' %} {% load widget_tweaks %} {% block title %} Start a New Topic {% endblock %} {% block breadcrumb %} <li class= "breadcrumb-item" ><a href= " {% url 'home' %} " > Boards </a></li> <li class= "breadcrumb-item" ><a href= " {% url 'board_topics' board.pk %} " > {{ board.name }} </a></li> <li class= "breadcrumb-item active" > New topic </li> {% endblock %} {% block content %} <form method= "post" novalidate > {% csrf_token %} {% for field in form %} <div class= "form-group" > {{ field.label_tag }} {% render_field field class = "form-control" %} {% if field.help_text %} <small class= "form-text text-muted" > {{ field.help_text }} </small> {% endif %} </div> {% endfor %} <button type= "submit" class= "btn btn-success" > Post </button> </form> {% endblock %}
它就是!所以,我们在这里使用django-widget-tweaks。首先,我们使用模板标记将其加载到模板中。然后用法:{ % load widget_tweaks % }
{% render_field field class = "form-control" %}
该render_field标签是没有的Django的一部分; 它住在我们安装的包装内。要使用它,我们必须传递一个表单字段实例作为第一个参数,然后我们可以添加任意HTML属性来补充它。它会很有用,因为我们可以根据某些条件分配类。
render_field模板标记的一些示例:
{% render_field form.subject class = "form-control" %} {% render_field form.message class = "form-control" placeholder = form.message.label %} {% render_field field class = "form-control" placeholder = "Write a message!" %} {% render_field field style = "font-size: 20px" %}
现在要实现Bootstrap 4验证标签,我们可以更改new_topic.html模板:
模板/ new_topic.html
<form method= "post" novalidate > {% csrf_token %} {% for field in form %} <div class= "form-group" > {{ field.label_tag }} {% if form.is_bound %} {% if field.errors %} {% render_field field class = "form-control is-invalid" %} {% for error in field.errors %} <div class= "invalid-feedback" > {{ error }} </div> {% endfor %} {% else %} {% render_field field class = "form-control is-valid" %} {% endif %} {% else %} {% render_field field class = "form-control" %} {% endif %} {% if field.help_text %} <small class= "form-text text-muted" > {{ field.help_text }} </small> {% endif %} </div> {% endfor %} <button type= "submit" class= "btn btn-success" > Post </button> </form>
结果如下:
所以,我们有三种不同的渲染状态:
- 初始状态:表单没有数据(没有绑定)
- 无效:我们添加.is-invalidCSS类并在带有类的元素中添加错误消息.invalid-feedback。表单字段和消息以红色呈现。
- 有效:我们添加.is-validCSS类,以便以绿色绘制表单字段,向用户反馈该字段是好的。
可重用的表单模板
模板代码看起来有点复杂,对吧?嗯,好消息是我们可以在整个项目中重用这个片段。
在templates文件夹中,创建一个名为includes的新文件夹:
myproject/ |-- myproject/ | |-- boards/ | |-- myproject/ | |-- templates/ | | |-- includes/ <-- here! | | |-- base.html | | |-- home.html | | |-- new_topic.html | | +-- topics.html | +-- manage.py +-- venv/
现在在includes文件夹中,创建一个名为form.html的文件:
模板/包括/ form.html
{% load widget_tweaks %} {% for field in form %} <div class= "form-group" > {{ field.label_tag }} {% if form.is_bound %} {% if field.errors %} {% render_field field class = "form-control is-invalid" %} {% for error in field.errors %} <div class= "invalid-feedback" > {{ error }} </div> {% endfor %} {% else %} {% render_field field class = "form-control is-valid" %} {% endif %} {% else %} {% render_field field class = "form-control" %} {% endif %} {% if field.help_text %} <small class= "form-text text-muted" > {{ field.help_text }} </small> {% endif %} </div> {% endfor %}
现在我们更改new_topic.html模板:
模板/ new_topic.html
{% extends 'base.html' %} {% block title %} Start a New Topic {% endblock %} {% block breadcrumb %} <li class= "breadcrumb-item" ><a href= " {% url 'home' %} " > Boards </a></li> <li class= "breadcrumb-item" ><a href= " {% url 'board_topics' board.pk %} " > {{ board.name }} </a></li> <li class= "breadcrumb-item active" > New topic </li> {% endblock %} {% block content %} <form method= "post" novalidate > {% csrf_token %} {% include 'includes/form.html' %} <button type= "submit" class= "btn btn-success" > Post </button> </form> {% endblock %}
顾名思义,它用于在另一个模板中包含 HTML模板。这是在项目中重用HTML组件的一种非常有用的方法。{ % include % }
我们实现的下一个表单,我们可以简单地使用它来呈现它。{ % include ‘includes/form.html’ % }
添加更多测试
现在我们正在使用Django Forms; 我们可以添加更多测试以确保它顺利运行:
板/ tests.py
# ... other imports from .forms import NewTopicForm class NewTopicTests ( TestCase ): # ... other tests def test_contains_form ( self ): # <- new test url = reverse ( 'new_topic' , kwargs = { 'pk' : 1 }) response = self . client . get ( url ) form = response . context . get ( 'form' ) self . assertIsInstance ( form , NewTopicForm ) def test_new_topic_invalid_post_data ( self ): # <- updated this one ''' Invalid post data should not redirect The expected behavior is to show the form again with validation errors ''' url = reverse ( 'new_topic' , kwargs = { 'pk' : 1 }) response = self . client . post ( url , {}) form = response . context . get ( 'form' ) self . assertEquals ( response . status_code , 200 ) self . assertTrue ( form . errors )
现在我们assertIsInstance第一次使用这种方法。基本上我们在上下文数据中抓取表单实例,并检查它是否是一个NewTopicForm。在上一次测试中,我们添加了self.assertTrue(form.errors)以确保表单在数据无效时显示错误。
结论
在本教程中,我们重点介绍了URL,可重用模板和表单。像往常一样,我们还实现了几个测试用例。这就是我们如何充满信心地发展。
我们的测试文件开始变大,所以在下一个教程中,我们将重构它以提高可维护性,从而维持代码库的增长。
我们也达到了需要与登录用户进行交互的程度。在下一个教程中,我们将学习有关身份验证以及如何保护我们的视图和资源的所有信息。
我希望你喜欢本教程系列的第三部分!第四部分将于2017年9月25日下周发布。如果您希望在第四部分发布时收到通知,您可以订阅我们的邮件列表。
该项目的源代码可在GitHub上获得。项目的当前状态可以在发布标记v0.3-lw下找到。以下链接将带您到正确的位置:
https://github.com/sibtc/django-beginners-guide/tree/v0.3-lw