自定义通用视图
web后端开发的工作就是对数据的增删改查!回顾前面的各种功能的代码,会发现有很多的代码冗余。我们在做重复的事情。django框架的一个强大的功能就是提供了一个即插即用的管理后台,django-admin。接下来,我去做一些代码的优化,抽取代码,自定义通用视图。
一、MyListView
后台管理功能中,总是需要以数据表的形式展示数据。复杂一点会有过滤,查询,分页等操作。但它们的业务逻辑一致,都是接收请求,然后去数据库中获取数据,再进行过滤,分页,然后展示。抽象出一个MyListView
来完成这些通用功能,我们只需要做少量配置就可以实现对不同的模型的数据展示。
1. 通用的类属性
找出通用属性,定义在类属性中,子视图类通过覆盖这些属性来完成自定义。
class MyListView(View):
model = None # 模型
template_name = None # 模板名称
is_paginate = False # 是否分页
per_page = None # 每页条数
page_header = None # 页头大标题
page_option = None # 页头小标题
table_title = None # 内容标题
fields = None # 需要展示的字段
2.方法
2.1 get
get方法是用来响应get请求的,根据请求,调用相应模型,查询数据然后渲染模板。
def get(self, request):
context = self.get_context_data()
return render(request, self.get_template_name(), context=context)
2.2 get_queryset
获取查询集,根据是否提供fields属性,来确定返回的数据。如果有搜索,过滤,可以复写该方法。
def get_queryset(self):
"""获取查询集,如需过滤,请复写此方法"""
if self.model is not None:
queryset = self.model._default_manager.all()
else:
raise ImproperlyConfigured(
"%(cls)s 是不是傻,你没有指定model,我怎么知道呢?" % {
'cls': self.__class__.__name__
}
)
if self.fields:
queryset = queryset.only(*self.fields)
return queryset
2.3 get_context_data
获取视图的上下文变量,如果需要额外添加,可以复写次方法。
def get_context_data(self, **kwargs):
"""获取视图的上下文变量,如要添加额外变量,请复写此方法"""
queryset = self.get_queryset()
if self.is_paginate:
page_size = self.per_page
if page_size:
page = self.paginate_queryset(queryset, page_size)
else:
page = self.paginate_queryset(queryset, 10)
else:
page = queryset
context = {
'page_obj': page,
'page_header': self.page_header,
'page_option': self.page_option,
'table_title': self.table_title
}
context.update(kwargs)
return context
2.4 get_template_name
获取模板名称,如果不提供类属性template_name,会根据模型名称拼接出默认的模板myadmin/model_name/model_name_list.html
def get_template_name(self):
"""获取模板名"""
if self.template_name is None:
self.template_name = 'myadmin/{0}/{0}_list.html'.format(self.model._meta.model_name)
return self.template_name
2.5 paginate_queryset
分页
def paginate_queryset(self, queryset, page_size):
"""如果需要进行分页"""
paginator = Paginator(queryset, page_size)
try:
page_number = int(self.request.GET.get('page', 1))
except Exception as e:
page_number = 1
page = paginator.get_page(page_number)
return page
3. 模板
在模板中将自定义的地方挖坑。obj_list.html
{% load static %}
{% load news_customer_filters %}
<section class="content-header">
<h1>
{{ page_header }}
<small>{{ page_option }}</small>
</h1>
</section>
<!-- Main content -->
<section class="content container-fluid">
<div class="box">
<div class="box-header with-border">
<h3 class="box-title">{{ table_title }}</h3>
<div class="box-tools">
{% block add_button %}
{% endblock %}
</div>
</div>
<!-- /.box-header -->
<div class="box-body">
{% block search_form %}
{% endblock %}
<table class="table table-bordered">
<tbody>
{% block table_content %}
{% endblock %}
</tbody>
</table>
</div>
<!-- 分页 -->
{% if page_obj.paginator %}
<div class="box-footer clearfix">
<div class="row">
<div class="col-sm-6">
<div class="dataTables_info" id="example2_info" role="status" aria-live="polite">
总共:{{ page_obj.paginator.count }}条 第{{ page_obj.start_index }}到{{ page_obj.end_index }}条
</div>
</div>
<div class="col-sm-6">
<ul class="pagination pagination-sm no-margin pull-right">
<li {% if not page_obj.has_previous %}class="disabled"{% endif %}
data-page="{{ page_obj.number|add:-1 }}"><a href="#">«</a></li>
{% for n in page_obj|page_bar %}
<li {% if n == page_obj.number %}class="active" {% endif %} data-page="{{ n }}"><a
href="#">{{ n }}</a></li>
{% endfor %}
<li {% if not page_obj.has_next %}class="disabled"{% endif %}
data-page="{{ page_obj.number|add:1 }}"><a href="#">»</a></li>
</ul>
</div>
</div>
</div>
{% endif %}
</div>
</section>
{% block script %}
<script>
$(() => {
{% block query_script %}
let $queryForm = $('form.user-query'); // 查询表单
let $queryBtn = $('form.user-query button.query'); // 查询按钮
let $resetBtn = $('form.user-query button.reset'); // 重置按钮
// 查询
$queryBtn.click(() => {
let url = $('.sidebar-menu li.active a').data('url');
if (!url) {
return
}
$
.ajax({
url: url,
data: $queryForm.serialize(),
type: 'GET'
})
.done((res) => {
$('#content').html(res)
})
.fail(() => {
message.showError('服务器超时,请重试!')
})
});
// 重置
$resetBtn.click(() => {
$queryForm[0].reset();
let url = $('.sidebar-menu li.active a').data('url');
if (!url) {
return
}
$
.ajax({
url: url,
type: 'GET'
})
.done((res) => {
$('#content').html(res)
})
.fail(() => {
message.showError('服务器超时,请重试!')
})
});
{% endblock query_script %}
// 分页
let $pageLi = $('ul.pagination li').not('.active').not('.disabled');
$pageLi.click(function () {
let $this = $(this);
$
.ajax({
url: $('.sidebar-menu li.active a').data('url'),
data: $queryForm.serialize() + '&page=' + $this.data('page'),
type: 'GET'
})
.done((res) => {
$('#content').html(res)
})
.fail(() => {
message.showError('服务器超时,请重试!')
})
});
// 实例详情
$('tr').each(function () {
$(this).children('td:first').click(function () {
let url = $(this).data('url');
if (!url) {
return
}
$
.ajax({
url: url,
type: 'GET'
})
.done((res) => {
if (res.errno === '4105') {
message.showError(res.errmsg)
} else if (res.errno === '4101') {
message.showError(res.errmsg);
setTimeout(() => {
window.location.href = res.data.url
}, 1000)
} else {
$('#content').html(res)
}
})
.fail(() => {
message.showError('服务器超时,请重试!')
})
})
});
// 添加实例
$('.box-tools button').click(function () {
let url = $(this).data('url');
if (!url) {
return
}
$
.ajax({
url: url,
type: 'GET'
})
.done((res) => {
if (res.errno === '4105') {
message.showError(res.errmsg)
} else if (res.errno === '4101') {
message.showError(res.errmsg);
setTimeout(() => {
window.location.href = res.data.url
}, 1000)
} else {
$('#content').html(res)
}
})
.fail(() => {
message.showError('服务器超时,请重试!')
})
});
});
</script>
{% endblock %}
<!-- /.content -->
4. 自定义过滤器
4.1 page_bar
根据分页页面对象生成分页页码
from django import template
register = template.Library()
@register.filter
def page_bar(page):
page_list = []
if page.number != 1:
page_list.append(1)
if page.number - 3 > 1:
page_list.append('...')
if page.number - 2 > 1:
page_list.append(page.number - 2)
if page.number - 1 > 1:
page_list.append(page.number - 1)
page_list.append(page.number)
if page.paginator.num_pages > page.number + 1:
page_list.append(page.number + 1)
if page.paginator.num_pages > page.number + 2:
page_list.append(page.number + 2)
if page.paginator.num_pages > page.number + 3:
page_list.append('...')
if page.paginator.num_pages != page.number:
page_list.append(page.paginator.num_pages)
return page_list
二、UpdateView
1. 通用的类属性
找出通用属性,定义在类属性中,子视图类通过覆盖这些属性来完成自定义。
class UpdateView(View):
model = None
form_class = None
template_name = None
fields = None # 需要修改的字段
page_header = None # 页头大标题
page_option = None # 页头小标题
table_title = None # 内容标题
pk = None # url路径参数名,默认pk 对象的pk字段
2.方法
2.1 get
get方法是用来响应get请求的,根据请求,获取模型对象,生成表单,渲染页面。
def get(self, request, **kwargs):
self.obj = self.get_obj(**kwargs)
context = self.get_context_data()
return render(request, self.get_template_name(), context=context)
2.2 put
put方法是用来响应put请求的,根据请求,获取模型对象,获取参数,填充表单,校验,修改对象。
def put(self, request, **kwargs):
self.obj = self.get_obj(**kwargs)
self.form_class = self.get_form_class()
form = self.form_class(QueryDict(request.body), instance=self.obj)
if form.is_valid():
self.save(form)
return json_response(errmsg='修改数据成功!')
else:
context = self.get_context_data(form=form)
return render(request, self.get_template_name(), context=context)
2.3 get_obj_id
获取传入的对象主键
def get_obj_id(self, **kwargs):
if self.pk is None:
self.obj_id = kwargs.get('pk')
else:
self.obj_id = kwargs.get(self.pk)
2.4 get_obj
根据传入的对象主键获取需要修改的对象
def get_obj(self, **kwargs):
self.get_obj_id(**kwargs)
if self.model is None:
raise ImproperlyConfigured('没有设置模型')
obj = self.model.objects.filter(pk=self.obj_id).first()
if not obj:
raise ObjectDoesNotExist('找不到pk=%s的对象' % self.obj_id)
return obj
2.5 get_template_name
获取模板名称,如果不提供类属性template_name,会根据模型名称拼接出默认的模板myadmin/model_name/model_name_detail.html
def get_template_name(self):
"""获取模板名"""
if self.template_name is None:
self.template_name = 'myadmin/{0}/{0}_detail.html'.format(self.model._meta.model_name)
return self.template_name
2.6 get_context_data
获取视图的上下文变量,如果需要额外添加,可以复写次方法。
def get_context_data(self, **kwargs):
self.form_class = self.get_form_class()
form = self.form_class(instance=self.obj)
context = {
'form': form,
'page_header': self.page_header,
'page_option': self.page_option,
'table_title': self.table_title
}
context.update(kwargs)
return context
2.7get_form_class
获取表单类,如果没有提供,通过给定字段属性自动生成
def get_form_class(self):
if self.form_class is None:
if self.fields is None:
raise ImproperlyConfigured('你有不设置form,又不设置fields字段,那怎么生成表单呢?')
return modelform_factory(self.model, fields=self.fields)
else:
return self.form_class
2.8 save
保存对象,如果有额外操作,复写此方法。
def save(self, form):
if form.has_changed():
instance = form.save(commit=False)
instance.save(update_fields=form.changed_data)
3.模板
{% load static %}
{% load news_customer_filters %}
{% load admin_customer_tags %}
{% load admin_customer_filters %}
<section class="content-header">
<h1>
{{ page_header }}
<small>{{ page_option }}</small>
</h1>
</section>
<!-- Main content -->
<section class="content container-fluid">
<div class="box">
<div class="box-header with-border">
<h3 class="box-title">{{ table_title }}</h3>
</div>
<!-- /.box-header -->
<div class="box-body">
<form class="form-horizontal">
{% csrf_token %}
{% block form_content %}
{% for field in form %}
{% if field|is_checkbox %}
<div class="form-group">
<div class="col-sm-offset-1 col-sm-11">
<div class="checkbox">
<label for="{{ field.id_for_label }}">{{ field }}{{ field.label }}</label>
</div>
</div>
</div>
{% elif field|is_url_field %}
<div class="form-group {% if field.errors %}has-error{% endif %}">
<label for="{{ field.id_for_label }}"
class="col-sm-1 control-label">{{ field.label }}</label>
<div class="col-sm-11">
{% for error in field.errors %}
<label class="control-label"
for="{{ field.id_for_label }}">{{ error }}</label>
{% endfor %}
<div class="input-group">
{% add_class field 'form-control' %}
<span class="input-group-btn"><input class="hidden" type="file">
<button type="button" class="btn btn-info btn-flat">上传文件</button>
</span>
</div>
</div>
</div>
{% else %}
<div class="form-group {% if field.errors %}has-error{% endif %}">
<label for="{{ field.id_for_label }}"
class="col-sm-1 control-label">{{ field.label }}</label>
<div class="col-sm-11">
{% for error in field.errors %}
<label class="control-label"
for="{{ field.id_for_label }}">{{ error }}</label>
{% endfor %}
{% add_class field 'form-control' %}
</div>
</div>
{% endif %}
{% endfor %}
{% endblock %}
</form>
</div>
<div class="box-footer">
<button type="button" class="btn btn-default back">返回</button>
<button type="button" {% if form.instance.id %}
data-url="{% block update_url %}{% endblock %}"
data-type="PUT"
{% else %}
data-url="{% block add_url %}{% endblock %}"
data-type="POST"
{% endif %}
class="btn btn-primary pull-right save">保存
</button>
</div>
</div>
</section>
{% block script %}
<script>
$(() => {
{% block back_button %}
// 返回按钮
$('.box-footer button.back').click(() => {
let url = $('.sidebar-menu li.active a').data('url');
if (!url) {
return
}
$('#content').load(
url,
(response, status, xhr) => {
if (status !== 'success') {
message.showError('服务器超时,请重试!')
}
}
);
});
{% endblock %}
{% block save_button %}
$('.box-footer button.save').click(function () {
let url = $(this).data('url');
if (!url) {
return
}
$
.ajax({
url: url,
data: $('form').serialize(),
type: $(this).data('type')
})
.done((res) => {
if (res.errno === '0') {
message.showSuccess(res.errmsg);
$('#content').load(
$('.sidebar-menu li.active a').data('url'),
(response, status, xhr) => {
if (status !== 'success') {
message.showError('服务器超时,请重试!')
}
}
);
} else {
$('#content').html(res)
}
})
.fail((res) => {
message.showError('服务器超时,请重试!')
})
});
{% endblock %}
{% block upload %}
// 上传文件input
let $fileInput = $('.input-group-btn input');
let $uploadBtn = $('.input-group-btn button');
$uploadBtn.click(function () {
$(this).prev('input[type="file"]').click()
}
);
// 自动上传文件
$fileInput.change(function () {
$this = $(this);
if ($this.val() !== '') {
let formData = new FormData();
formData.append('upload', $this[0].files[0]);
formData.append('csrfmiddlewaretoken', $('input[name="csrfmiddlewaretoken"]').val());
$
.ajax({
url: '/admin/upload/',
// 使用ckeditor_uploader 就使用下面的url
// url: '/ckeditor/upload/&responseType=json',
type: 'POST',
data: formData,
processData: false,
contentType: false
})
.done((res) => {
if (res.uploaded === '1') {
message.showSuccess('封面图片上传成功!');
$this.parent().prev('input').val(res.url);
// 清空一下
$this.val('')
} else {
message.showError('封面图片上传失败!')
}
})
.fail(() => {
message.showError('服务器超时, 请重新尝试!')
})
}
});
{% endblock %}
});
</script>
{% endblock %}
<!-- /.content -->
模板中用到了自定义过滤器和自定义标签
4. 自定义过滤器和自定义标签
4.1 is_checkbox
判断字段是否checkbox 类型,决定渲染方式
from django.template import Library
from django.forms.widgets import CheckboxInput
register = Library()
@register.filter()
def is_checkbox(field):
return isinstance(field.field.widget, CheckboxInput)
4.2 is_url_field
判断字段是否url类型,决定渲染方式
@register.filter()
def is_url_field(field):
return True if 'url' in field.label else False
4.3add_class
给form字段添加class样式
from django.template import Library
from django.shortcuts import reverse
register = Library()
@register.simple_tag()
def add_class(field, class_str):
return field.as_widget(attrs={'class': class_str})
三、AddView
1.通用类属性
class AddView(View):
model = None
form_class = None
template_name = None
fields = None # 新建对象需要的字段
page_header = None # 页头大标题
page_option = None # 页头小标题
table_title = None
2.方法
2.1 get
渲染一个对于模型的表单页面返回。
def get(self, request):
context = self.get_context_data()
return render(request, self.get_template_name(), context=context)
2.2 post
根据post参数生成表单,校验,创建新对象。
def post(self, request):
self.form_class = self.get_form_class()
form = self.form_class(request.POST)
if form.is_valid():
self.save(form)
return json_response(errmsg='添加数据成功!')
else:
context = self.get_context_data(form=form)
return render(request, self.get_template_name(), context=context)
2.3 get_context_data
获取视图的上下文变量,如果需要额外添加,可以复写次方法。
def get_context_data(self, **kwargs):
self.form_class = self.get_form_class()
form = self.form_class()
context = {
'form': form,
'page_header': self.page_header,
'page_option': self.page_option,
'table_title': self.table_title
}
context.update(kwargs)
return context
2.4 get_template_name
获取模板名称,如果不提供类属性template_name,会根据模型名称拼接出默认的模板`myadmin/model_name/model_name_detail.html
def get_template_name(self):
"""获取模板名"""
if self.template_name is None:
self.template_name = 'myadmin/{0}/{0}_detail.html'.format(self.model._meta.model_name)
return self.template_name
2.5 get_form_class
获取表单类,如果没有提供,通过给定字段属性自动生成
def get_form_class(self):
if self.form_class is None:
if self.fields is None:
raise ImproperlyConfigured('你有不设置form,又不设置fields字段,那怎么生成表单呢?')
return modelform_factory(self.model, fields=self.fields)
else:
return self.form_class
2.6 save
保存对象,如果有额外操作,复写此方法。
def save(self, form):
form.save()
3. 模板
与UpdateView视图一致
四、继续抽象
仔细观察上面的三个视图,发现有很多共同的方法,还需要将这些代码进一步的抽离,然后通过继承来达到减少冗余和解耦的作用。
1.TemplateView
class TemplateView(View):
model = None # 模型
template_name = None
page_header = None # 页头大标题
page_option = None # 页头小标题
table_title = None # 内容标题
fields = None # 需要展示的字段
def get(self, request):
context = self.get_context_data()
return render(request, self.get_template_name(), context=context)
def get_context_data(self, **kwargs):
context = {
'page_header': self.page_header,
'page_option': self.page_option,
'table_title': self.table_title
}
context.update(kwargs)
return context
def get_template_name(self):
"""获取模板名"""
if self.template_name is None:
if isinstance(self, MyListView):
self.template_name = 'myadmin/{0}/{0}_list.html'.format(self.model._meta.model_name)
else:
self.template_name = 'myadmin/{0}/{0}_detail.html'.format(self.model._meta.model_name)
return self.template_name
2. DetailView
class DetailView(View):
form_class = None
def get_form_class(self):
if self.form_class is None:
if self.fields is None:
raise ImproperlyConfigured('你有不设置form,又不设置fields字段,那怎么生成表单呢?')
return modelform_factory(self.model, fields=self.fields)
else:
return self.form_class
def save(self, form):
form.save()
3.MyListView
class MyListView(TemplateView):
is_paginate = False # 是否分页
per_page = None # 每页条数
def get_queryset(self):
"""获取查询集,如需过滤,请复写此方法"""
if self.model is not None:
queryset = self.model._default_manager.all()
else:
raise ImproperlyConfigured(
"%(cls)s 是不是傻,你没有指定model,我怎么知道呢?" % {
'cls': self.__class__.__name__
}
)
if self.fields:
queryset = queryset.only(*self.fields)
return queryset
def get_context_data(self, **kwargs):
"""获取视图的上下文变量,如要添加额外变量,请继承此方法"""
queryset = self.get_queryset()
if self.is_paginate:
page_size = self.per_page
if page_size:
page = self.paginate_queryset(queryset, page_size)
else:
page = self.paginate_queryset(queryset, 10)
else:
page = queryset
return super().get_context_data(page_obj=page)
def paginate_queryset(self, queryset, page_size):
"""如果需要进行分页"""
paginator = Paginator(queryset, page_size)
try:
page_number = int(self.request.GET.get('page', 1))
except Exception as e:
page_number = 1
page = paginator.get_page(page_number)
return page
4.UpdateView
class UpdateView(TemplateView, DetailView):
pk = None # url路径参数名,默认pk 对象的pk字段
def get(self, request, **kwargs):
self.obj = self.get_obj(**kwargs)
return super().get(request)
def put(self, request, **kwargs):
self.obj = self.get_obj(**kwargs)
self.form_class = self.get_form_class()
form = self.form_class(QueryDict(request.body), instance=self.obj)
if form.is_valid():
self.save(form)
return json_response(errmsg='修改数据成功!')
else:
context = self.get_context_data(form=form)
return render(request, self.get_template_name(), context=context)
def get_obj_pk(self, **kwargs):
if self.pk is None:
self.obj_id = kwargs.get('pk')
else:
self.obj_id = kwargs.get(self.pk)
def get_obj(self, **kwargs):
self.get_obj_pk(**kwargs)
if self.model is None:
raise ImproperlyConfigured('没有设置模型')
obj = self.model.objects.filter(pk=self.obj_id).first()
if not obj:
raise ObjectDoesNotExist('找不到pk=%s的对象' % self.obj_id)
return self.model.objects.filter(pk=self.obj_id).first()
def get_context_data(self, **kwargs):
self.form_class = self.get_form_class()
form = self.form_class(instance=self.obj)
context = super().get_context_data(form=form)
context.update(**kwargs)
return context
def save(self, form):
if form.has_changed():
instance = form.save(commit=False)
instance.save(update_fields=form.changed_data)
5. AddView
class AddView(TemplateView, DetailView):
def post(self, request):
self.form_class = self.get_form_class()
form = self.form_class(request.POST)
if form.is_valid():
self.save(form)
return json_response(errmsg='添加数据成功!')
else:
context = self.get_context_data(form=form)
return render(request, self.get_template_name(), context=context)
def get_context_data(self, **kwargs):
self.form_class = self.get_form_class()
form = self.form_class()
context = super().get_context_data(form=form)
context.update(**kwargs)
return context