本文链接:https://www.jianshu.com/p/230c652fc5a2
作者:西瓜甜
一、认识基本的 CBV
CBV 就是 Class Base View ,基于类的视图。
基于类的视图提供另一种将视图实现为 Python 对象而不是函数的方法。它们不能替代基于函数的视图,但与基于函数的视图相比,它们是有某些不同和优势的。
与特定的 HTTP 方法(GET, POST, 等等)关联的代码组织能通过单独的方法替代条件分支来解决。
面向对象技术(比如 mixins 多重继承)可用于将代码分解为可重用组件。
本质上来说,基于类的视图允许你使用不同的类实例方法响应不同 HTTP 请求方法,而不是在单个视图函数里使用有条件分支的代码。
因此在视图函数里处理 HTTP GET 的代码应该像下面这样:
from django.http import HttpResponse
def my_view(request):
if request.method == 'GET':
# <view logic>
return HttpResponse('result')
而在基于类的视图里,会变成:
from django.http import HttpResponse
from django.views import View
class MyView(View):
def get(self, request):
# <view logic>
return HttpResponse('result')
类视图工作流程:
因为 Django 的 URL 解析器期望发送请求和相关参数来调动函数而不是类,基于类的视图有一个
as_view()
类方法,当一个请求到达的 URL 被关联模式匹配时,这个类方法返回一个函数。这个函数创建一个类的实例,调用setup()
初始化它的属性,然后调用dispatch()
方法。dispatch
观察请求并决定它是GET
和POST
,等等。如果它被定义,那么依靠请求来匹配方法,否则会引发HttpResponseNotAllowed
。
# urls.py
from django.urls import path
from myapp.views import MyView
urlpatterns = [
path('about/', MyView.as_view()),
]
说明:
CBV 的基于方法的视图返回值和基于函数的视图(FBV)返回值是相同的,既某种形式的HttpResponse
。
内置通用显示视图
示例 model
from django.db import models
from django.utils import timezone
# Create your models here.
from users.models import UsersProfile
class ArticlePost(models.Model):
'''
博客文章数据模型
'''
# 文章作者。参数 on_delete 用于指定数据删除的方式
author = models.ForeignKey(UsersProfile, related_name="users", verbose_name="作者 ID", on_delete=models.CASCADE)
# 文章标题。models.CharField 为字符串字段,用于保存较短的字符串,比如标题
title = models.CharField("标题", max_length=100)
# 文章正文。保存大量文本使用 TextField
body = models.TextField("正文")
# 文章浏览量 PositiveIntegerField是用于存储正整数的字段
views_counts = models.PositiveIntegerField("浏览量", default=0)
# 文章创建时间。参数 default=timezone.now 指定其在创建数据时将默认写入当前的时间
created = models.DateTimeField("创建时间", auto_now_add=True)
# 文章更新时间。参数 auto_now=True 指定每次数据更新时自动写入当前时间
updated = models.DateTimeField("更新时间", auto_now=True)
# 内部类 class Meta 用于给 model 定义元数据
class Meta:
# ordering 指定模型返回的数据的排列顺序
# '-created' 表明数据应该以倒序排列
ordering = ('-created',)
verbose_name = '文章'
verbose_name_plural = verbose_name
db_table = "article_post"
# 函数 __str__ 定义当调用对象的 str() 方法时的返回值内容
def __str__(self):
# return self.title 将文章标题返回
return f"{self.title}"
class Tag(models.Model):
"""
文章标签模型
"""
name = models.CharField("标签名称", max_length=12)
aticle = models.ManyToManyField(ArticlePost, related_name="tag")
class Meta:
verbose_name = '标签'
verbose_name_plural = '标签'
db_table = 'tag'
def __str__(self):
return f"{self.name}"
ListView
返回一个模型对象列表。
可以指定模板文件路径名
自定义返回给前端模板语言中使用的对象的变量名
视图
class ArticlePostListView(ListView):
# 指定获取数据的 model
model = ArticlePost
# 指定模板语言中使用的变量名
context_object_name = 'postList'
# 指明模板名称, 默认是 `model所在的应用名/model 名称的小写_list.html`
template_name = 'blog/post-list.html'
说明:
模板文件要放置在设置好的 templates 正确目录下。
路由
path('post_list/', ArticlePostListView.as_view(), name='postList'),
模板
这里使用的是 Bootstrap4
<table class="table table-hover">
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">文章标题</th>
<th scope="col">作者名字</th>
<th scope="col">浏览量</th>
<th scope="col">创建日期</th>
</tr>
</thead>
<tbody>
{% for article in postList %}
<tr>
<th scope="row">{{ forloop.counter }}</th>
<td>{{ article.id }}</td>
<td>{{ article.author.username }}</td>
<td>{{ article.views_counts }}</td>
<td>{{ article.created }}</td>
</tr>
{% endfor %}
</tbody>
</table>
对返回的对象列表进行过滤
指定
model = ArticlePost
实际上是
queryset = ArticlePost.objects.all()
的简写。
所以,通过使用queryset
属性可以对返回的对象列表进行过滤控制。
例如下面的示例只是返回了以id
字段进行倒序排列的对象列表
class ArticlePostListView(ListView):
queryset = ArticlePost.objects.order_by("-id")
context_object_name = 'postList'
template_name = 'blog/post-list.html'
其他的都不需要改变
动态过滤
大多数情况下,我们会通过 URL 给视图传递参数,视图通过这个参数作为过滤条件。
当基于类的视图被调用的时候,各种常用的东西被存储在 self
上,而且请求 (self.request
) 会根据(self.args) 从 URLconf 中抓取位置参数,根据 (self.kwargs) 抓取基于名称的关键字参数。
示例
URL
path('post_list/<author>/', ArticleAuthorPostListView.as_view(), name='postAuthorList'),
VIEW
当然,仅仅这样做还不够,接下来需要在视图中覆盖 LlistView
中的 了解我get_queryset()
方法。这个方法用于返回 queryset
属性的值。
我们可以在这个方法里添加更多的逻辑。
from django.shortcuts import get_object_or_404
from django.views import View
from blog.models import ArticlePost,
from users.models import UsersProfile
class ArticleAuthorPostListView(ListView):
context_object_name = 'postList'
template_name = 'blog/post-list.html'
def get_queryset(self):
# 从用户 model 中获取到对应的作者对象
self.author = get_object_or_404(UsersProfile, username=self.kwargs['author'])
# 根据作者对象返回其所有的文章
return ArticlePost.objects.filter(author__username=self.author)
前端
模板中的路由写法
<a class="nav-link" href="{% url 'cbv:postAuthorList' 'author2' %}">ListAuthorView</a>
返回的模板还是使用上例中使用的模板
获取 GET 请求的参数
# views.py
uid = self.request.GET.get("id")
添加额外的数据
重写父类的 get_context_data
方法可以给返回前端模板的数据中增加额外的数据。
get_context_data(** kwargs)
会返回一个用于显示上下文数据的对象。
是包含以下内容的字典:
object
:此视图正在展示的对象(self.object
)。context_object_name
上下文对象的名字
示例,在返回 model 中对象的同时,返回当前的时间。
from django.utils import timezone
from django.views.generic import ListView
class ArticlePostListView(ListView):
queryset = ArticlePost.objects.order_by("-id")
context_object_name = 'postList'
template_name = 'blog/post-list.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['now'] = timezone.now()
return context
模板中使用
context['now']
中的now
就是模板中使用的变量名
<span>Date: {{ now }}</span>
DetailView
用于通过条件过滤,获取到的 Model 中的单个对象,就是表中的一条数据。
同样可以指定返回的模板文件和模板语言中使用的数据变量名。
可以添加额外的数据。
注意需要从 URLconf 中捕获到参数,一般是用于基于主键的查找。
注意和 ListView 中添加过滤的区别,同样都是过滤,但是 DetailView 返回的是
单个对象
,就是一条数据
。
而 ListView 始终返回一组数据,即便这组数据中只有一条数据,返回的形式任然是一个包含了列表的 Qeuryset 对象。
Example myapp/urls.py:
path('post_detail/<slug:pk>/', ArticlePostDetailView.as_view(), name='postDetail'),
Example myapp/views.py:
from django.views.generic import DetailView
class ArticlePostDetailView(DeleteView):
model = ArticlePost
context_object_name = 'postDetail'
template_name = 'blog/post-detail.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['now'] = timezone.now()
return context
Example myapp/article_detail.html:
<h1> {{ postDetail.title}}</h1>
<p>{{ postDetail.author.username }}</p>
<p>日期: {{ now |date }}</p>
<p>{{ postDetail.body }}</p>
注意:
<slug:pk>
中的pk
, 是 URLconf 传给视图的一个关键字参数, 比如pk=1
。
DetailView 中的get_object
方法会依据该值执行基于主键的查找。
相当于model.Mymodelname.objects.filter(id=pk)
通过非主键字段进行过滤(扩展知识)
如果找不到参数
pk
,它会在 URLconf 查找一个slug_url_kwarg
指定的参数,默认值是slug
,并使用slug_field
的值, 默认是slug
。执行一个slug 查找, 这也表名了会进行非主键的过滤查找 。
相当于model.Mymodelname.objects.filter(slug=slug)
依据上面的意思,一般我们的 model 中不会存在 slug
字段,因此,我们往往会对 slug_field
的值进行更改,改为自己 model 中存在的,假设想使用此字段进行过滤查找的情况下。比如下面我们使用字段 updated
, 可以这样修改:
path('post_detail/<slug:slug>/', ArticlePostDetailView.as_view(), name='postDetail'),
class ArticlePostDetailView(DeleteView):
...
# 设置 model 中的字段名
slug_field = 'updated'
...
这样的情况下,查询数据库会是这样的
ArticlePost.objects.filter(updated=sulg)
> 注:slug 会是 url 中传递过来的值,比如 2019-07-01
注意: DetailView 视图的
get_object
方法只允许返回单个对象。
因此, 必须保证这个字段和其对应的值,只能在数据库表中查的一条数据。
添加额外的动作
有时候,经常会在访问一个视图的时候,可能想更新操作的 model 中的某个字段。
比如在 ArticlePost 中有个 last_accessed 字段,用来记录这条数据最后一次被访问的时间
class ArticlePostDetailView(DeleteView):
model = ArticlePost
context_object_name = 'postDetail'
template_name = 'blog/post-detail.html'
queryset = ArticlePost.objects.all()
def get_object(self):
obj = super().get_object()
# Record the last accessed date
obj.last_accessed = timezone.now()
obj.save()
return obj
验证类
Form
基本示例:
计划是这样的,使用 FormView 来验证一个用户注册的数据有效性。
1. 前端页面效果图

2. 前端代码
基于 Bootstrap4
<form action="{% url 'users:register' %}" method="post">
{% csrf_token %}
<div class="form-row">
<div class="form-group col-md-6">
<label for="inputEmail4">用户名</label>
<input type="text" name="username" value="{{ form.username.value|default:'' }}" class="form-control" id="inputEmail4" placeholder="用户名">
{% if form.errors.username %}
<span class="alert alert-danger" role="alert">
{{ form.errors.username.0 }}
</span>
{% endif %}
</div>
<div class="form-group col-md-6">
<label for="inputPassword4">密码</label>
<input type="password" name="password" class="form-control" id="inputPassword4" placeholder="密码">
{% if form.errors.password %}
<span class="alert alert-danger" role="alert">
{{ form.errors.password.0 }}
</span>
{% endif %}
</div>
</div>
<div class="form-group">
<label for="inputAddress">邮箱地址</label>
<input type="email" value="{{ form.email.value|default:'' }}" name="email" class="form-control" id="inputAddress" placeholder="邮箱地址">
{% if form.errors.email %}
<span class="alert alert-danger" role="alert">
{{ form.errors.email.0 }}
</span>
{% endif %}
</div>
<div class="form-group">
<label for="inputAddress2">手机号</label>
<input type="text" name="mobile" value="{{ form.mobile.value|default:'' }}" class="form-control" id="inputAddress2" placeholder="手机号">
{% if form.errors.mobile %}
<span class="alert alert-danger" role="alert">
{{ form.errors.mobile.0 }}
</span>
{% endif %}
</div>
<button type="submit" class="btn btn-primary">注册</button>
</form>
<input type="email" value="{{ form.email.value|default:'' }}" name="email" class="form-control" id="inputAddress" placeholder="邮箱地址">
{{ form.email.value|default:'' }}
假如验证不通过,会保存上次输入的值
其中default:''
是个过滤器,当第一次 GET 请求时,并没有向表单中输入任何值,Django 回返回None
, 这里会把None
改为空字符串''
。
{{ form.errors.email.0 }}
假如这个数据提交的是错误的,这里会显示具体的错误信息。
错误示例如下图

4. 这个 form 类是这样的
注意:
这里的 类属性必须和前端
input
标签中的name
属性同名。
比如
<input type="password" name="password">
中name
的值是 password
下面类中的属性也要是password
# registor_forms.py
from django import forms
class RegiterUserForm(forms.Form):
username = forms.CharField(
# 必须的值
required=True,
)
password = forms.CharField(
required=True,
)
email = forms.EmailField(
required=True,
)
mobile = forms.CharField(
min_length=11,
max_length=11,
required=True,
)
使用 FormView
构建视图
之后可以使用
FormView
结合刚才的RegiterUserForm
构建这个用于验证用户注册的视图:
# views.py
from django.urls import reverse_lazy # 用于路由反转,把名称空间解析为 URL
from users.users_forms.register_form import RegiterUserForm
from django.views.generic.edit import FormView
class UserRegisterView(FormView):
# GET 请求时,返回此 模板
template_name = 'register.html'
# 自定义的Form 类
form_class = RegiterUserForm
# 提交数据成功后跳转到此 url
success_url = reverse_lazy('users:login')
def form_valid(self, form):
# 数据有效,存库
UsersProfile.objects.create(**form.cleaned_data)
return super().form_valid(form)
def form_invalid(self, form):
# 数据无效返回原来的模板,并且携带原来提交的数据
return super().form_invalid(form)
注意:
FormView
继承了TemplateResponseMixin
,因此可以在这里使用template_name
。
form_valid()
默认实现了简单的重定向至success_url
。
从模型中创建表单
假如你对已有的 model 进行表单的验证,无需像之前使用 forms.Form
中那样定义每个字段的属性,应用在 model 中已经定义过了。
使用 ModelForm
可以让这件事做起来易如反掌。
model 示例
假设有如下 model, 用于记录用户的留言
from django.utils import timezone
from django.db import models
class UserAsk(models.Model):
name = models.CharField('姓名', max_length=20)
mobile = models.CharField('手机', max_length=11)
article_name = models.CharField('文章名', max_length=50)
content = models.TextField('留言内容')
add_time = models.DateTimeField('添加时间', default=timezone.now)
class Meta:
verbose_name_plural = '用户留言'
db_table = 'user_ask'
def __str__(self):
return self.name
创建表
# 项目根目录下执行如下命令
# 1. 生成数据移植文件
on git:master x$ python3 manage.py makemigrations
# 2. 创建表结构
on git:master x$ python3 manage.py migrate
接下来创建一个自定义的 Form 类,需要继承 forms.ModelForm
from django import forms
from blog.models import UserAsk
class UserAskForm(forms.ModelForm):
class Meta:
model = UserAsk
fields = ['name', 'mobile', 'content', 'article_name']
model
属性指定自己的 modelfieldes
属性指定要要验证哪些字段
这里还可添加一些额外的自定义验证,比如验证手机号的合法性
clean_<fieldname>()
方法是在表单子类上调用的,其中 <fieldname>
替换为表单字段属性的名称。
此方法可以实现对此字段除了 Form 本身验证之外的任何特定验证需求,而与该所属字段属性的类型无关。
此方法不传递任何参数。您需要在 self.cleaned_data
中获取该字段的值,并记住此时它将是一个Python对象,而不是表单中提交的原始字符串(因为上面的常规表单验证方法已经验证过一次数据,并对数据类型进行正确转换了)。
例如,如果要验证名为 serialnumber
的字段的字符内容是否唯一,则 clean_serialnumber()
方法 将是执行此操作的正确方法名称。
此方法的返回值将替换已 cleaned_data
中的现有值,因此它必须是已 cleaned_data
中字段的值(即使此方法未更改它)或新的已清理值。
import re
from django import forms
from blog.models import UserAsk
class UserAskForm(forms.ModelForm):
class Meta:
model = UserAsk
fields = ['name', 'mobile', 'article_name','content']
# 针对指定字段添加自定义验证
# 方法名格式要求: clean_字段名
def clean_mobile(self):
"""
验证手机号是否合法
:return: 合法的数据或者错误信息
"""
mobile = self.cleaned_data['mobile']
PRGEX_MOBILE = r'^1[358]\d{9}|^147\d{8}|^176\d{8}$'
regex = re.compile(PRGEX_MOBILE)
if regex.match(mobile):
return mobile
else:
raise forms.ValidationError(
'无效的手机号',
code='mobile_invalid'
)
使用
路由
form .views import UserAskView
path('user_ask', UserAskView.as_view(), name='user_ask'),
视图
class UserAskView(FormView):
# GET 请求时,返回此 模板
template_name = 'blog/user_ask.html'
# 自定义的Form 类
form_class = UserAskForm
# 提交数据成功后跳转到此 url
success_url = reverse_lazy('cbv:user_ask')
def form_valid(self, form):
# 数据有效
# 使用 save 方法可直接存入数据库
form.save()
return super().form_valid(form)
def form_invalid(self, form):
# 数据无效返回原来的模板,并且携带原来提交的数据
print(form.errors)
return super().form_invalid(form)
save() 方法
每个 ModelForm 都有一个save()
方法,此方法根据绑定到表单的数据创建并保存数据库对象。
HTML 模板
关于更新
ModelForm 的子类可接受一个现有的模型实例作为关键字参数 instance
的值;如果提供了,则 save()
会更新这个实例。如果没有,则 save() 会创建一个对应模型的新实例。
>>> from myapp.models import Article
>>> from myapp.forms import ArticleForm
# Create a form instance from POST data.
>>> f = ArticleForm(request.POST)
# Save a new Article object from the form's data.
>>> new_article = f.save()
# Create a form to edit an existing Article, but use
# POST data to populate the form.
>>> a = Article.objects.get(pk=1)
>>> f = ArticleForm(request.POST, instance=a)
>>> f.save()
save() 方法接受一个可选参数 commit ,它的值是 True 或者 False 。默认是 True。
调用 save() 的时候使用 commit=False ,那么它会返回一个尚未保存到数据库的对象。
ModelForm 与普通的表单工作方式一样。例如,用 is_valid() 方法来检查合法性,用 is_multipart() 方法来确定表单是否需要multipart文件上传(之后是否必须将 request.FILES 传递给表单),等等。
选择要使用的字段
Django 强烈建议您使用 fields 属性来显式设置所有应在表单中编辑的字段。
这是处于安全考虑。
- 明确指明使用所有的字段进行验证
fields = '__all__'
- 明确的排除不用验证的字段
exclude = ['article_name']
覆盖默认字段的属性
自定义小部件
如果您希望 Author 的 name 属性的 CharField 由 <textarea> 代替默认的 <input type="text"> 来表示,您可以重写字段的部件:
from django.forms import ModelForm, Textarea
from myapp.models import Author
class AuthorForm(ModelForm):
class Meta:
model = Author
fields = ('name', 'title', 'birth_date')
widgets = {
'name': Textarea(attrs={'cols': 80, 'rows': 20}),
}
自定义错误信息
from django.utils.translation import gettext_lazy as _
class AuthorForm(ModelForm):
class Meta:
model = Author
fields = ('name', 'title', 'birth_date')
labels = {
'name': _('Writer'),
}
help_texts = {
'name': _('Some useful help text.'),
}
error_messages = {
'name': {
'max_length': _("This writer's name is too long."),
},
}
提供初始值
与普通表单一样,可以在实例化表单时通过指定 initial
参数来指定表单的初始值。以这种方式提供的初始值会覆盖表单字段的初始值以及对应模型实例的初始值。例如:
>>>> article = Article.objects.get(pk=1)
>>> article.headline
'My headline'
>>> form = ArticleForm(initial={'headline': 'Initial headline'}, instance=article)
>>> form['headline'].value()
'Initial headline'</pre>
装饰基于类的视图
因为基于类的视图不是函数,所以根据你是使用 as_view() 还是创建子类,装饰它们的工作方式会有不同。
在 URLconf 中装饰
装饰基于类的视图最简单的方式是装饰 as_view()
方法的结果。最方便的地方是在你部署视图的 URLconf 中执行此操作。
from django.contrib.auth.decorators import login_required, permission_required
from django.views.generic import TemplateView
from .views import VoteView
urlpatterns = [
path('about/', login_required(TemplateView.as_view(template_name="secret.html"))),
path('vote/', permission_required('polls.can_vote')(VoteView.as_view())),
]
这个方式在每个基本实例上应用装饰器。如果你想装饰视图的每个实例,你需要采用不同方式。
装饰类
装饰基于类的视图的每个实例,你需要装饰类定义本身。为此,你可以将装饰器应用到类的 dispatch()
方法。
类上的方法与独立函数完全不同,因此你不能应用函数装饰器到类的方法上——需要先将它转换为方法装饰器。method_decorator
装饰器会将函数装饰器转换为方法装饰器,这样它就被用在实例方法上。举例:
from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
from django.views.generic import TemplateView
class ProtectedView(TemplateView):
template_name = 'secret.html'
@method_decorator(login_required)
def dispatch(self, *args, **kwargs):
return super().dispatch(*args, **kwargs)
或者,更简洁的说,你可以用装饰类来代替,并作为关键参数 name 传递要被装饰的方法名:
from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
from django.views.generic import TemplateView
@method_decorator(login_required, name='dispatch')
class ProtectedView(TemplateView):
template_name = 'secret.html'
若果在一个地方使用了多个装饰器,可以定义一个含有多个函数装饰器的列表,之后使用它,而不是多次调用
method_decorator
。
这两个类是等价的:
# good
decorators = [never_cache, login_required]
@method_decorator(decorators, name='dispatch')
class ProtectedView(TemplateView):
template_name = 'secret.html'
# bad
@method_decorator(never_cache, name='dispatch')
@method_decorator(login_required, name='dispatch')
class ProtectedView(TemplateView):
template_name = 'secret.html'
装饰器将按照它们传递给装饰器的顺序来处理请求。在这个例子里,never_cache()
将在 login_required()
之前处理请求。
特殊视图跳过 CSRF 验证
from django.http import JsonResponse
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_exempt
from django.views import View
@method_decorator(csrf_exempt, name='dispatch')
class AssetView(View):
def get(self, request):
return JsonResponse({"get": "GET"})
def post(self, request):
data = json.loads(request.body.decode())
print(data)
return JsonResponse({"error": "ok"})
网友评论