新增一个 APP
博客算是一个功能集,因此我们应将其体现为一个模块。这表现在 Django 应用里则是为其创建一个 APP Package。现在让 manage.py 中的 startapp 闪亮登场:
1
|
$ python manage.py startapp blog |
这样,工程根目录下会增加一个 blog package 并包含如下文件/目录:
其中:
- migrations 下包含对模型定义与修改的迁移记录;
- admin.py 是对 Django 站点管理的定制;
- apps.py 包含对 App 的配置;
- models.py 应包含定义的模型;
- tests.py 包含单元测试;
- views.py 应包含各种视图。
注意释义中的“应”这个字,这是代表不一定要完全这么做,他们只是 Django 建议的结构。事实上“怎么玩”完全可以取决于自己,进而打造一种属于自己的代码结构。然后还需要做一件事,就是注册这个新的 APP,这样在运行工程的时候 Django 才会去识别它。打开 myblog/settings.py,在 INSTALLED_APPS 添加 blog:
1
2
3
4
5
6
7
8
9
10
11
12
|
# Application definition INSTALLED_APPS = [
'django.contrib.admin' ,
'django.contrib.auth' ,
'django.contrib.contenttypes' ,
'django.contrib.sessions' ,
'django.contrib.messages' ,
'django.contrib.staticfiles' ,
'blog' ,
] |
铸造基石 —— 模型的建立
从分析“要实现什么”中,我们应该对这个要实现的系统有一个整体的认识与把控。接下来首先要做的就是通过需求来建立模型,即 M 层,它决定了数据的存取。之前的需求中存在这样的一句话:
博客拥有标题、作者、正文及发布的时间等基本的元素。同时,一个博客还会隶属于一个分类,并可能包含一个或多个标签。博客也可以有一个或多个评论,评论中要记录评论者的称呼、邮箱以及评论内容;
从中可以看出:
- 直接与博客相关的模型应包含三个:博客、分类以及标签。博客包含标题、作者、正文、发布时间等基础字段。分类和标签则应有名称;
- 一个博客仅隶属于一个分类,反过来一个分类则可以包含多个博客,这是明显的一对多的关系,因此博客中应有一个外键关联至分类;
- 一个博客可能包含一个或多个标签,反过来一个标签页会包含多个博客,因此是多对多的关系;
- 还有一个评论模型来存储用户评论,并主动关联到博客上去。一个博客可以有一个或多个评论,但反过来评论只针对一篇博客,因此也是一对多的关系。
接下来我们可以以此建立对应的模型,最基础的代码如下:
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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
|
from django.db import models
class Category(models.Model):
"""
分类
"""
name = models.CharField( '名称' , max_length = 16 )
class Tag(models.Model):
"""
标签
"""
name = models.CharField( '名称' , max_length = 16 )
class Blog(models.Model):
"""
博客
"""
title = models.CharField( '标题' , max_length = 32 )
author = models.CharField( '作者' , max_length = 16 )
content = models.TextField( '正文' )
created = models.DateTimeField( '发布时间' , auto_now_add = True )
category = models.ForeignKey(Category, verbose_name = '分类' )
tags = models.ManyToManyField(Tag, verbose_name = '标签' )
class Comment(models.Model):
"""
评论
"""
blog = models.ForeignKey(Blog, verbose_name = '博客' )
name = models.CharField( '称呼' , max_length = 16 )
email = models.EmailField( '邮箱' )
content = models.CharField( '内容' , max_length = 140 )
created = models.DateTimeField( '发布时间' , auto_now_add = True )
|
模型只是利用 Django 提供的 ORM 完成对实际表结构的映射,因此在完成模型定义后,我们需要将其真正同步到实际的数据库中去。新版本的 Django 中,该操作需要分成两步,第一步是生成 migrations:
1
|
$ python manage.py makemigrations blog |
会生成如下所示的文件:
然后执行 migrate 命令来进行数据库的同步操作:
1
2
3
4
5
6
|
$ python35 manage.py migrate Operations to perform: Apply all migrations: admin, contenttypes, auth, blog, sessions
Running migrations: Rendering model states... DONE
Applying blog.0001_initial... OK
|
注意最后一行,说明新的表已经成功同步至数据库。
添加内容
既然存储博客的地方已经有了,现在是时候尝试往里面插入一些数据了,这也方便之后在实现功能之后进行测试。传统的方式可以选择手动连接到数据库,写原生的 SQL 来完成该步骤。当然还有一个更“酷”的方法就是利用 Django 内置的站点管理工具。(在前一章中我在最后的“发现更多”中提到了站点管理并建议你做一些简单的尝试,如果忽略了这一步的话,在做接下来的操作之前,你可能需要先运行 manage.py createsuperuser 命令创建一个超级管理员账户)。
我们已经注意到在 app 目录下的 admin.py 文件,要想让定义的模型在站点管理中展示,就需要在此将所需的模型显示的添加进去。打开 blog/admin.py 文件并添加如下内容:
1
2
3
4
5
6
|
from django.contrib import admin
from .models import Category, Tag, Blog
admin.site.register([Category, Tag, Blog]) |
重新运行 runserver 开发服务器并通过 /admin/ 访问后台,应该能看到我们所期望的内容:
第三篇:专注功能的实现之博客列表的展示
通过前面所做的各种铺垫,我们现在可以来专注功能的实现了,即剩下的 V 层和 T 层。这一部分我们着重关注博客列表页。首先来回顾一下需求:
需要有一个“博客列表”页来呈现发布的博客。博客要按发布时间的倒序来排列,每个博客都要包含标题、作者、分类、发布时间的显示(年-月-日 时:分)及节选的正文内容(前 100 个字)。点击单独的博客可以进入其详情页。
在实际的开发前,我们应能看到产品部门针对各页面所做的原型图,而 UI 部门也会遵循此完成设计图。因此我们就能清楚的知道页面中包含哪些元素、应如何摆放。但这里因为只是一个简单的演示,所以请允许我“索性随意发挥”了。
建立视图
视图就是所谓的 V 层,他负责分析并处理来自用户的请求,然后返回所需的结果。“博客列表”的视图显然是将数据库中的数据按需求中所需的发布时间的倒序取出,然后构造模板,最终将结果返回给用户。现在就让我们来编写它吧。
1
2
3
4
5
6
7
8
9
10
|
from django.shortcuts import render
from .models import Blog
def get_blogs(request):
ctx = {
'blogs' : Blog.objects. all ().order_by( '-created' )
}
return render(request, 'blog-list.html' , ctx)
|
这里,我们使用了 Django 的 ORM 对博客数据进行查询,并依发布时间的倒序进行排列,然后将结果集(一个 QuerySet 对象)放在一个字典对象 ctx 的 blogs 中。然后调用 render 方法来渲染模板,其第一个参数始终接收 request 对象,第二个参数是要渲染的模板文件,第三个参数则是可选的上下文参数。注意观察视图函数的入参,其第一个参数一定是 request 对象,“这是一个不变的真理”。
定义模板
现在就只剩下 T 层没有完成了。废话不多说,在工程根目录下新建一个 templates 文件夹,并在其中新增一个 blog-list.html 文件,这个文件的名字要和之前视图函数中 render 方法的入参中定义的相对应。 然后打开并写入以下代码:
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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
|
<!DOCTYPE html> < html lang = "en" >
< head >
< meta charset = "UTF-8" >
< title >My Blogs</ title >
< style >
.blog {
padding: 20px 0px;
}
.blog .info span {
padding-right: 10px;
}
.blog .summary {
padding-top: 20px;
}
</ style >
</ head >
< body >
< div class = "header" >
< h1 >My Blogs</ h1 >
</ div >
{% for blog in blogs %} < div class = "blog" >
< div class = "title" >
< a href = "#" >< h2 >{{ blog.title }}</ h2 ></ a >
</ div >
< div class = "info" >
< span class = "category" style = "color: #ff9900;" >{{ blog.category.name }}</ span >
< span class = "author" style = "color: #4a86e8" >{{ blog.author }}</ span >
< span class = "created" style = "color: #6aa84f" >{{ blog.created|date:"Y-m-d H:i" }}</ span >
</ div >
< div class = "summary" >
{{ blog.content|truncatewords:100 }}
</ div >
</ div >
{% endfor %} </ body >
</ html >
|
正如你所看到的,我们作为上下文传入的参数在模板中可以以变量的形式直接访问。这里我们对 blogs 进行了一次 for 迭代,然后为每一篇博客生成一个 div 容器供其展示。在 div 内部又通过变量属性的访问来获取诸如博客标题、分类名称、作者、正文和发布时间等内容。注意这里用到了很多有意思的模板标签,这些都值得你去一步步挖掘。
另外,在博客标题上有一个 <a> 标签,其中的 href 属性只填写了一个固定的“#”。理论上来说,这里应该填入每篇博客对应的详情页链接,但因为到目前为止我们还没有实现出“博客详情”页,所以先这么临时处理,待之后再完善。
完成后续工作
现在,MTV 三层我们都已完成了,是时候验证一下到目前为止的成果。不过在此之前还有一个很重要的工作就是为这个视图指定一个发现它的路径,这样用户才能通过在浏览器的地址栏中输入地址并正确访问到他。这个重要的工作是由 URLConf 来帮助我们完成的。打开 myblog/urls.py 并在 urlpatterns 中增加一条:
1
2
3
4
5
|
urlpatterns = [
...
url(r '^$' , get_blogs, name = 'blog_get_blogs' ),
] |
这里定义的是在根路径下就可以访问到博客列表页。现在让我们重新运行开发服务器并打开浏览器尝试访问一下博客列表页。不过,如果我没猜错的话,这时屏幕上出现的内容并不是我们期待的。
这是 DEBUG 模式下(默认是开启的,通过 settings 中的 DEBUG 配置项来定义)的错误提示页,也就是传说中的 500 错误。在这种模式下,Django 会为我们打印出问题的根源所在:TemplateDoesNotExist。嗯?之前我们不是都定义好了的吗?为什么会说找不到呢?很简单,因为我们没有真正告诉 Django 应到哪里去寻找我们定义的模板文件。这需要在 settings 中的 TEMPLATES 配置项中声明。现在打开 myblog/settings.py 文件并将 TEMPLATES 配置项修改成如下这样:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
TEMPLATES = [
{
'BACKEND' : 'django.template.backends.django.DjangoTemplates' ,
'DIRS' : [ '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 的模型中确实默认就有这么一个唯一的且未自增长的主键,即 id 字段。
我们可以以此为关键字来定位一篇博客。那还有一个问题就是如何才能收到该关键字呢?这时就要利用到带通配符的 URL 定义并使用圆括号把参数在 URL 模式里标识出来,而标识出来的内容就会以参数的形式传入到视图函数中。比如这样:
1
|
(r '^detail/(\d+)/$' , get_detail, name = 'blog_get_detail' ),
|
圆括号中匹配到的数字就会传入到 detail 的第二个参数中去(还记得吗?第一个参数始终为 request 对象)。
如何定位一篇博客的问题解决了,还有另一个问题摆在我们面前:如何生成和处理用户的评论?这应当很快联想到 Django Form 能帮助我们快速实现这一需求。因此目前的任务就是来构建一个表单。新建一个文件 blog/forms.py,并写入以下代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
from django import forms
class CommentForm(forms.Form):
"""
评论表单
"""
name = forms.CharField(label = '称呼' , max_length = 16 , error_messages = {
'required' : '请填写您的称呼' ,
'max_length' : '称呼太长'
})
email = forms.EmailField(label = '邮箱' , error_messages = {
'required' : '请填写您的邮箱' ,
'invalid' : '邮箱格式不正确'
})
content = forms.CharField(label = '评论内容' , error_messages = {
'required' : '请填写您的评论内容' ,
'max_length' : '评论内容太长'
})
|
代码中定义了一个评论表单的类并根据需求定义了三个字段:称呼、邮箱和评论内容。这样我们就能利用它来快速生成表单并验证用户的输入。
到此几个小屏障都扫清了,可以开始编写视图函数了:
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
|
from django.http import Http404
from .forms import CommentForm
def get_detail(request, blog_id):
try :
blog = Blog.objects.get( id = blog_id)
except Blog.DoesNotExist:
raise Http404
if request.method = = 'GET' :
form = CommentForm()
else :
form = CommentForm(request.POST)
if form.is_valid():
cleaned_data = form.cleaned_data
cleaned_data[ 'blog' ] = blog
Comment.objects.create( * * cleaned_data)
ctx = {
'blog' : blog,
'comments' : blog.comment_set. all ().order_by( '-created' ),
'form' : form
}
return render(request, 'blog-detail.html' , ctx)
|
该视图函数要比之前的列表函数复杂一些,我们以代码中的空行为界来对其进行拆分并分析:
- get_detail() 视图函数首先利用传入的 blog_id 到数据库中查询该条博客记录。注意这使用了 try 块,因为 model.objects.get() 方法会在未能查询到数据的情况下抛出 model.DoesNotExist 的异常。如果没有对此异常进行拦截的话,就会导致服务器 500 错误,这显然不是用户想看到的。而是应该在发生此错误的情况下通知用户“你要访问的内容未能找到”。一般情况下网站都会在这个时候向用户抛出一个 404 错误并定义一些“生动有趣”的话语来提示用户同时又不会显得太突兀而让用户产生反感;
- 中间一个部分是对表单的初始化和校验处理。这里会涉及到 HTTP 的 GET 请求和 POST 请求。一般来说,GET 请求用于获取数据,而 POST 请求则为提交数据。这里也遵循此一般约定,在用户做 GET 请求时,仅初始化一个空表单供用户填写;而如果是 POST 请求的话,则在初始化的同时将用户传入的数据传入。用户传入的数据会收集在 request.POST 中,是一个类似于字典的对象。接下来是调用 form 类的 is_valid() 方法来对用户输入做校验,如果校验成功,则创建一条评论记录;
- ctx 依然是要传入到模板的上下文参数,其中 blog 是博客对象,comments 是利用 ORM 的反查方法找到当前博客包含的所有评论,并以发布时间的倒序方式进行排列,另外还有一个 form,是之前定义的 CommentForm 的实例化对象,它用于初始化评论表单。
最后来定义模板。先来看代码,新建 templates/blog-detail.html 并写入如下内容:
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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
|
<!DOCTYPE html> < html lang = "en" >
< head >
< meta charset = "UTF-8" >
< title >{{ blog.title }}</ title >
< style >
.blog {
padding: 20px 0px;
}
.blog .info span {
padding-right: 10px;
}
.blog .summary {
padding-top: 20px;
}
</ style >
</ head >
< body >
< div class = "header" >
< span >< a href = "{% url 'blog_get_blogs' %}" >博客</ a > - < a href = "{% url 'blog_get_detail' blog.id %}" >{{ blog.title }}</ a ></ span >
</ div >
< div class = "content" >
< div class = "blog" >
< div class = "title" >
< a href = "#" >< h2 >{{ blog.title }}</ h2 ></ a >
</ div >
< div class = "info" >
< span class = "category" style = "color: #ff9900;" >{{ blog.category.name }}</ span >
< span class = "author" style = "color: #4a86e8" >{{ blog.author }}</ span >
< span class = "created" style = "color: #6aa84f" >{{ blog.created|date:"Y-m-d H:i" }}</ span >
</ div >
< div class = "summary" >
{{ blog.content }}
</ div >
</ div >
< div class = "comment" >
< div class = "comments-display" style = "padding-top: 20px;" >
< h3 >评论</ h3 >
{% for comment in comments %}
< div class = "comment-field" style = "padding-top: 10px;" >
{{ comment.name }} 说: {{ comment.content }}
</ div >
{% endfor %}
</ div >
< div class = "comment-post" style = "padding-top: 20px;" >
< h3 >提交评论</ h3 >
< form action = "{% url 'blog_get_detail' blog.id %}" method = "post" >
{% csrf_token %}
{% for field in form %}
< div class = "input-field" style = "padding-top: 10px" >
{{ field.label }}: {{ field }}
</ div >
< div class = "error" style = "color: red;" >
{{ field.errors }}
</ div >
{% endfor %}
< button type = "submit" style = "margin-top: 10px" >提交</ button >
</ form >
</ div >
</ div >
</ div >
</ body >
</ html >
|
这里对几个新出现的模板语言/标签进行一个简要的解释:
- {% url 'blog_get_blogs' %} 可以看作是 reverse 方法的“模板语言”版,其作用是根据 URLConf 中的 name 定义对 url 进行反解析,转换成真实的 URL 地址。比如这里在转换之后会变成 “/”。如果所指定的 url 定义中包含参数,则需要将参数跟在后面,如 {% url 'blog_get_detail' blog.id %};
- 观察表单的内容,{% csrf_token %} 用于防跨域请求,可参考一些关于 CSRF 相关的资料并阅读 Django 官方文档关于这一块实现的描述。{% for field in form %}{% endfor %} 是对表单的各个 field 进行迭代并生成相应的表单元素,并对校验过程中出现的错误进行显示以提示用户做相应的修改。最后还生成一个 button 用于点击提交评论。
来看看最终的效果:
最后,记得将 template/blog-list.html 中博客标题上 a 标签中的 href 也改为 {% url 'blog_get_detail' blog.id %},当用户点击标题时就可以直接进入详情页面了
总结一下:
1.新建项目
2.新建app
3.安装app
4.创建models 里的类
5.migrate数据表
6.admin里面注册顺便加上文章内容
7.views里面新建get blog 方法将models里面的文章传给模板文件
8.创建模板文件并更改urls.py
9.增加models里面的blog 的id属性
10.新建form.py
11.getdetail 通过 form传来的数据保存到comment然后传到 detail模板文件渲染
12.更改urls.py