You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Django/doc/blog/views.py

506 lines
24 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 导入Python内置logging模块用于记录视图运行日志如缓存命中、错误信息
import logging
# 导入Python内置os模块用于文件路径操作如文件上传时创建目录
import os
# 导入Python内置uuid模块用于生成唯一文件名避免文件上传时重名
import uuid
# 导入Django项目配置模块用于获取项目设置如分页数量、文件上传路径
from django.conf import settings
# 导入Django分页类用于处理评论分页逻辑
from django.core.paginator import Paginator
# 导入DjangoHTTP响应类
# 1. HttpResponse返回普通响应
# 2. HttpResponseForbidden返回403禁止访问响应
from django.http import HttpResponse, HttpResponseForbidden
# 导入Django快捷函数
# 1. get_object_or_404获取对象不存在则返回404页面
# 2. render渲染模板并返回响应
from django.shortcuts import get_object_or_404
from django.shortcuts import render
# 导入Django静态文件模板标签用于生成静态文件URL如上传文件的访问URL
from django.templatetags.static import static
# 导入Django时区工具用于处理时间相关操作如文件上传目录按日期划分
from django.utils import timezone
# 导入Django国际化翻译工具用于错误信息的多语言支持
from django.utils.translation import gettext_lazy as _
# 导入Django CSRF豁免装饰器用于文件上传接口避免CSRF验证
from django.views.decorators.csrf import csrf_exempt
# 导入Django通用视图
# 1. DetailView详情页通用视图适用于单条数据展示如文章详情
# 2. ListView列表页通用视图适用于多条数据展示如文章列表、分类列表
from django.views.generic.detail import DetailView
from django.views.generic.list import ListView
# 导入Haystack搜索视图用于实现全文搜索功能
from haystack.views import SearchView
# 从当前应用导入核心模型:文章、分类、链接显示类型、友情链接、标签
from blog.models import Article, Category, LinkShowType, Links, Tag
# 从comments应用导入评论表单用于文章详情页的评论提交
from comments.forms import CommentForm
# 从自定义工具模块导入工具函数:
# 1. cache缓存操作对象用于读写缓存
# 2. get_blog_setting获取博客全局配置如评论分页数量
# 3. get_sha256生成SHA256加密字符串用于文件上传签名验证
from djangoblog.utils import cache, get_blog_setting, get_sha256
# 创建日志记录器日志名称与当前模块一致__name__便于定位日志来源
logger = logging.getLogger(__name__)
# 定义文章列表基础视图类ArticleListView继承自Django的ListView列表页通用视图
# 作用:封装所有列表类视图(首页、分类列表、标签列表、作者列表)的公共逻辑(分页、缓存、上下文处理)
class ArticleListView(ListView):
# template_name指定列表页渲染模板所有子类可复用或重写
template_name = 'blog/article_index.html'
# context_object_name指定模板中使用的上下文变量名模板中通过{{ article_list }}访问列表数据)
context_object_name = 'article_list'
# page_type页面类型标识用于模板显示标题如"分类目录归档"),子类需重写
page_type = ''
# paginate_by分页数量从项目配置中读取settings.PAGINATE_BY
paginate_by = settings.PAGINATE_BY
# page_kwarg分页参数名URL中用于传递页码的参数默认'page'
page_kwarg = 'page'
# link_type友情链接显示类型关联LinkShowType枚举控制不同页面显示不同链接
link_type = LinkShowType.L
# 定义获取视图缓存key的方法当前未实际使用预留扩展
def get_view_cache_key(self):
return self.request.get['pages']
# 页码属性:通过@property装饰器将方法转为属性统一获取当前页码从URL参数或默认值1
@property
def page_number(self):
page_kwarg = self.page_kwarg
# 从URL路径参数、GET参数中获取页码均无则默认1
page = self.kwargs.get(
page_kwarg) or self.request.GET.get(page_kwarg) or 1
return page
# 定义获取查询集缓存key的抽象方法子类必须重写确保缓存key唯一
def get_queryset_cache_key(self):
"""
子类重写.获得queryset的缓存key
"""
raise NotImplementedError()
# 定义获取查询集数据的抽象方法(子类必须重写,实现具体数据查询逻辑)
def get_queryset_data(self):
"""
子类重写.获取queryset的数据
"""
raise NotImplementedError()
# 从缓存中获取查询集数据(缓存命中则直接返回,未命中则查询并缓存)
def get_queryset_from_cache(self, cache_key):
'''
缓存页面数据
:param cache_key: 缓存key唯一标识缓存数据
:return: 文章列表数据(从缓存或数据库获取)
'''
value = cache.get(cache_key) # 尝试从缓存获取数据
if value:
# 缓存命中:记录日志并返回数据
logger.info('get view cache.key:{key}'.format(key=cache_key))
return value
else:
# 缓存未命中调用子类实现的get_queryset_data获取数据库数据
article_list = self.get_queryset_data()
cache.set(cache_key, article_list) # 将数据存入缓存
logger.info('set view cache.key:{key}'.format(key=cache_key))
return article_list
# 重写ListView的get_queryset方法从缓存获取数据替代默认直接查询数据库
def get_queryset(self):
'''
重写默认,从缓存获取数据
:return: 文章列表查询集
'''
key = self.get_queryset_cache_key() # 获取子类定义的缓存key
value = self.get_queryset_from_cache(key) # 从缓存获取数据
return value
# 重写ListView的get_context_data方法添加额外上下文变量友情链接显示类型
def get_context_data(self, **kwargs):
kwargs['linktype'] = self.link_type # 传递友情链接显示类型到模板
# 调用父类方法,保留原有上下文数据(如分页数据、文章列表)
return super(ArticleListView, self).get_context_data(** kwargs)
# 定义首页视图类IndexView继承自ArticleListView复用列表页公共逻辑
class IndexView(ArticleListView):
'''
首页视图:展示已发布的普通文章列表
'''
# 重写友情链接显示类型首页显示LinkShowType.I
link_type = LinkShowType.I
# 实现父类抽象方法:获取首页文章数据(查询已发布的普通文章)
def get_queryset_data(self):
# 筛选条件type='a'普通文章、status='p'(已发布)
article_list = Article.objects.filter(type='a', status='p')
return article_list
# 实现父类抽象方法生成首页缓存key包含页码确保不同分页缓存独立
def get_queryset_cache_key(self):
cache_key = 'index_{page}'.format(page=self.page_number)
return cache_key
# 定义文章详情页视图类ArticleDetailView继承自Django的DetailView详情页通用视图
class ArticleDetailView(DetailView):
'''
文章详情页面视图:展示单篇文章详情、评论列表及评论分页
'''
template_name = 'blog/article_detail.html' # 详情页渲染模板
model = Article # 关联的模型(自动从数据库查询文章数据)
pk_url_kwarg = 'article_id' # URL中传递文章ID的参数名与路由配置一致
context_object_name = "article" # 模板中使用的上下文变量名({{ article }}访问文章数据)
# 重写DetailView的get_object方法获取文章对象后更新浏览量
def get_object(self, queryset=None):
# 调用父类方法获取文章对象
obj = super(ArticleDetailView, self).get_object()
obj.viewed() # 调用Article模型的viewed方法浏览量+1
self.object = obj # 保存文章对象到实例属性
return obj
# 重写DetailView的get_context_data方法添加评论表单、评论列表、分页等额外上下文
def get_context_data(self, **kwargs):
comment_form = CommentForm() # 初始化评论表单(供用户提交评论)
# 获取当前文章的评论列表从缓存或数据库Article模型的comment_list方法已实现缓存
article_comments = self.object.comment_list()
# 筛选顶级评论parent_comment=None无父评论的评论
parent_comments = article_comments.filter(parent_comment=None)
# 获取博客全局配置(如文章页评论分页数量)
blog_setting = get_blog_setting()
# 初始化评论分页器(按配置的评论数量分页)
paginator = Paginator(parent_comments, blog_setting.article_comment_count)
# 从GET参数获取评论页码默认1若参数非法则重置为1
page = self.request.GET.get('comment_page', '1')
if not page.isnumeric():
page = 1
else:
page = int(page)
if page < 1:
page = 1
if page > paginator.num_pages:
page = paginator.num_pages
# 获取当前页的评论数据
p_comments = paginator.page(page)
# 计算下一页、上一页页码无则为None
next_page = p_comments.next_page_number() if p_comments.has_next() else None
prev_page = p_comments.previous_page_number() if p_comments.has_previous() else None
# 生成下一页评论URL含锚点#commentlist-container直接跳转到评论区
if next_page:
kwargs[
'comment_next_page_url'] = self.object.get_absolute_url() + f'?comment_page={next_page}#commentlist-container'
# 生成上一页评论URL
if prev_page:
kwargs[
'comment_prev_page_url'] = self.object.get_absolute_url() + f'?comment_page={prev_page}#commentlist-container'
# 向上下文添加额外数据(供模板使用)
kwargs['form'] = comment_form # 评论表单
kwargs['article_comments'] = article_comments # 所有评论
kwargs['p_comments'] = p_comments # 当前页评论
kwargs['comment_count'] = len(
article_comments) if article_comments else 0 # 评论总数
# 上一篇、下一篇文章从缓存获取Article模型的next_article/prev_article方法已实现缓存
kwargs['next_article'] = self.object.next_article
kwargs['prev_article'] = self.object.prev_article
# 调用父类方法,保留原有上下文数据(如文章对象)
return super(ArticleDetailView, self).get_context_data(** kwargs)
# 定义分类详情视图类CategoryDetailView继承自ArticleListView
class CategoryDetailView(ArticleListView):
'''
分类目录列表视图:展示指定分类及子分类下的已发布文章
'''
page_type = "分类目录归档" # 页面类型标识(模板中显示该标题)
# 实现父类抽象方法:获取分类下的文章数据
def get_queryset_data(self):
# 从URL路径参数获取分类slug与路由配置的<slug:category_name>对应)
slug = self.kwargs['category_name']
# 获取分类对象不存在则返回404
category = get_object_or_404(Category, slug=slug)
categoryname = category.name # 分类名称
self.categoryname = categoryname # 保存到实例属性供后续生成缓存key和上下文使用
# 获取当前分类的所有子分类名称(含自身)
categorynames = list(
map(lambda c: c.name, category.get_sub_categorys()))
# 筛选条件分类名称在子分类列表中、状态为已发布status='p'
article_list = Article.objects.filter(
category__name__in=categorynames, status='p')
return article_list
# 实现父类抽象方法生成分类列表缓存key含分类名称和页码确保缓存唯一
def get_queryset_cache_key(self):
slug = self.kwargs['category_name']
category = get_object_or_404(Category, slug=slug)
categoryname = category.name
self.categoryname = categoryname
cache_key = 'category_list_{categoryname}_{page}'.format(
categoryname=categoryname, page=self.page_number)
return cache_key
# 重写父类的get_context_data方法添加分类相关上下文页面类型、分类名称
def get_context_data(self, **kwargs):
categoryname = self.categoryname
# 处理分类名称(若含'/',取最后一部分,避免多级分类显示异常)
try:
categoryname = categoryname.split('/')[-1]
except BaseException:
pass
kwargs['page_type'] = CategoryDetailView.page_type # 页面类型
kwargs['tag_name'] = categoryname # 分类名称(模板中显示)
return super(CategoryDetailView, self).get_context_data(** kwargs)
# 定义作者详情视图类AuthorDetailView继承自ArticleListView
class AuthorDetailView(ArticleListView):
'''
作者文章列表视图:展示指定作者发布的已发布文章
'''
page_type = '作者文章归档' # 页面类型标识
# 实现父类抽象方法生成作者文章列表缓存key
def get_queryset_cache_key(self):
from uuslug import slugify # 延迟导入slugify函数避免循环导入
# 从URL路径参数获取作者名称转换为slug格式确保缓存key统一
author_name = slugify(self.kwargs['author_name'])
cache_key = 'author_{author_name}_{page}'.format(
author_name=author_name, page=self.page_number)
return cache_key
# 实现父类抽象方法:获取指定作者的文章数据
def get_queryset_data(self):
# 从URL路径参数获取作者名称
author_name = self.kwargs['author_name']
# 筛选条件作者用户名匹配、类型为普通文章type='a'、状态为已发布status='p'
article_list = Article.objects.filter(
author__username=author_name, type='a', status='p')
return article_list
# 重写父类的get_context_data方法添加作者相关上下文页面类型、作者名称
def get_context_data(self, **kwargs):
author_name = self.kwargs['author_name']
kwargs['page_type'] = AuthorDetailView.page_type # 页面类型
kwargs['tag_name'] = author_name # 作者名称(模板中显示)
return super(AuthorDetailView, self).get_context_data(** kwargs)
# 定义标签详情视图类TagDetailView继承自ArticleListView
class TagDetailView(ArticleListView):
'''
标签列表视图:展示指定标签下的已发布文章
'''
page_type = '分类标签归档' # 页面类型标识
# 实现父类抽象方法:获取标签下的文章数据
def get_queryset_data(self):
# 从URL路径参数获取标签slug
slug = self.kwargs['tag_name']
# 获取标签对象不存在则返回404
tag = get_object_or_404(Tag, slug=slug)
tag_name = tag.name # 标签名称
self.name = tag_name # 保存到实例属性
# 筛选条件标签名称匹配、状态为已发布status='p'
article_list = Article.objects.filter(
tags__name=tag_name, type='a', status='p')
return article_list
# 实现父类抽象方法生成标签列表缓存key含标签名称和页码
def get_queryset_cache_key(self):
slug = self.kwargs['tag_name']
tag = get_object_or_404(Tag, slug=slug)
tag_name = tag.name
self.name = tag_name
cache_key = 'tag_{tag_name}_{page}'.format(
tag_name=tag_name, page=self.page_number)
return cache_key
# 重写父类的get_context_data方法添加标签相关上下文页面类型、标签名称
def get_context_data(self, **kwargs):
tag_name = self.name # 从实例属性获取标签名称
kwargs['page_type'] = TagDetailView.page_type # 页面类型
kwargs['tag_name'] = tag_name # 标签名称(模板中显示)
return super(TagDetailView, self).get_context_data(** kwargs)
# 定义文章归档视图类ArchivesView继承自ArticleListView
class ArchivesView(ArticleListView):
'''
文章归档页面视图:展示所有已发布文章(按时间归档)
'''
page_type = '文章归档' # 页面类型标识
paginate_by = None # 关闭分页(归档页显示所有文章,不分页)
page_kwarg = None # 无需分页参数
template_name = 'blog/article_archives.html' # 归档页专用模板
# 实现父类抽象方法:获取所有已发布文章
def get_queryset_data(self):
return Article.objects.filter(status='p').all()
# 实现父类抽象方法生成归档页缓存key无页码因不分页
def get_queryset_cache_key(self):
cache_key = 'archives'
return cache_key
# 定义友情链接列表视图类LinkListView继承自Django的ListView
class LinkListView(ListView):
model = Links # 关联Links模型
template_name = 'blog/links_list.html' # 友情链接页面模板
# 重写get_queryset方法仅查询已启用的友情链接
def get_queryset(self):
return Links.objects.filter(is_enable=True)
# 定义搜索视图类EsSearchView继承自Haystack的SearchView
class EsSearchView(SearchView):
# 重写get_context方法构建搜索结果页面的上下文数据分页、搜索关键词、拼写建议
def get_context(self):
# 构建分页器和当前页数据Haystack内置方法
paginator, page = self.build_page()
# 基础上下文数据:搜索关键词、搜索表单、分页数据、拼写建议
context = {
"query": self.query, # 搜索关键词
"form": self.form, # 搜索表单
"page": page, # 当前页搜索结果
"paginator": paginator, # 分页器
"suggestion": None, # 拼写建议默认None
}
# 若搜索引擎支持拼写建议,获取建议词
if hasattr(self.results, "query") and self.results.query.backend.include_spelling:
context["suggestion"] = self.results.query.get_spelling_suggestion()
# 添加额外上下文数据(子类可扩展)
context.update(self.extra_context())
return context
# CSRF豁免装饰器关闭CSRF验证文件上传接口通常由第三方工具调用无需CSRF令牌
@csrf_exempt
def fileupload(request):
"""
文件上传接口:提供图床功能,支持图片/文件上传返回文件访问URL
注意需通过调用端传递正确签名才能上传仅允许POST请求
:param request: HTTP请求对象
:return: 上传成功返回文件URL列表失败返回403/错误信息
"""
# 仅允许POST请求文件上传需用POST
if request.method == 'POST':
# 从GET参数获取签名用于验证上传权限
sign = request.GET.get('sign', None)
if not sign:
return HttpResponseForbidden() # 无签名返回403禁止访问
# 验证签名双重SHA256加密项目SECRET_KEY与传递的sign比对防止未授权上传
if not sign == get_sha256(get_sha256(settings.SECRET_KEY)):
return HttpResponseForbidden() # 签名不匹配返回403
response = [] # 存储上传成功的文件URL
# 遍历请求中的所有上传文件(支持多文件同时上传)
for filename in request.FILES:
# 生成日期字符串(按年/月/日划分上传目录,便于管理)
timestr = timezone.now().strftime('%Y/%m/%d')
# 支持的图片格式后缀
imgextensions = ['jpg', 'png', 'jpeg', 'bmp']
# 文件名字符串处理
fname = u''.join(str(filename))
# 判断是否为图片文件(检查文件名是否包含图片后缀)
isimage = len([i for i in imgextensions if fname.find(i) >= 0]) > 0
# 定义文件保存根目录:
# - 图片文件保存到 static/files/image/年/月/日
# - 其他文件保存到 static/files/files/年/月/日
base_dir = os.path.join(settings.STATICFILES, "files" if not isimage else "image", timestr)
# 若目录不存在,创建多级目录
if not os.path.exists(base_dir):
os.makedirs(base_dir)
# 生成唯一文件名UUID+原文件后缀(避免重名)
savepath = os.path.normpath(os.path.join(base_dir, f"{uuid.uuid4().hex}{os.path.splitext(filename)[-1]}"))
# 安全校验:确保保存路径在基础目录内(防止路径穿越攻击)
if not savepath.startswith(base_dir):
return HttpResponse("only for post")
# 写入文件:分块写入(处理大文件上传,避免内存溢出)
with open(savepath, 'wb+') as wfile:
for chunk in request.FILES[filename].chunks():
wfile.write(chunk)
# 若为图片文件,压缩图片(降低文件大小,提升访问速度)
if isimage:
from PIL import Image # 延迟导入PIL库图片处理
image = Image.open(savepath)
image.save(savepath, quality=20, optimize=True) # 质量20开启优化
# 生成文件访问URL通过static标签生成静态文件URL
url = static(savepath)
response.append(url) # 将URL添加到响应列表
# 返回URL列表字符串格式
return HttpResponse(response)
else:
# 非POST请求返回错误信息
return HttpResponse("only for post")
# 404页面未找到视图处理所有不存在的URL请求
def page_not_found_view(
request,
exception,
template_name='blog/error_page.html'):
if exception:
logger.error(exception) # 记录异常信息到日志
url = request.get_full_path() # 获取用户访问的不存在的URL
# 渲染404错误页面传递错误信息和状态码
return render(request,
template_name,
{'message': _('Sorry, the page you requested is not found, please click the home page to see other?'),
'statuscode': '404'},
status=404)
# 500服务器错误视图处理服务器内部错误
def server_error_view(request, template_name='blog/error_page.html'):
# 渲染500错误页面传递错误信息和状态码
return render(request,
template_name,
{'message': _('Sorry, the server is busy, please click the home page to see other?'),
'statuscode': '500'},
status=500)
# 403权限拒绝视图处理无权限访问的请求
def permission_denied_view(
request,
exception,
template_name='blog/error_page.html'):
if exception:
logger.error(exception) # 记录异常信息到日志
# 渲染403错误页面传递错误信息和状态码
return render(
request, template_name, {
'message': _('Sorry, you do not have permission to access this page?'),
'statuscode': '403'}, status=403)
# 清理缓存视图:清空所有缓存(用于手动刷新缓存)
def clean_cache_view(request):
cache.clear() # 调用缓存工具的clear方法清空所有缓存
return HttpResponse('ok') # 返回成功响应