yxy添加 views.py 注释

yxy_branch
严欣怡 4 months ago
parent 88f904992d
commit 8571e2971b

@ -1,379 +1,498 @@
import logging
import os
import uuid
import uuid # 用于生成唯一文件名
# 导入Django核心模块配置、分页、HTTP响应、视图工具、翻译等
from django.conf import settings
from django.core.paginator import Paginator
from django.http import HttpResponse, HttpResponseForbidden
from django.shortcuts import get_object_or_404
from django.shortcuts import render
from django.templatetags.static import static
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from django.views.decorators.csrf import csrf_exempt
from django.views.generic.detail import DetailView
from django.views.generic.list import ListView
from haystack.views import SearchView
from django.shortcuts import get_object_or_404, render
from django.templatetags.static import static # 生成静态文件URL
from django.utils import timezone # 处理时间
from django.utils.translation import gettext_lazy as _ # 国际化翻译
from django.views.decorators.csrf import csrf_exempt # 豁免CSRF验证用于文件上传
from django.views.generic.detail import DetailView # 详情页通用视图
from django.views.generic.list import ListView # 列表页通用视图
from haystack.views import SearchView # 搜索视图
# 导入项目模型、表单、工具和插件
from blog.models import Article, Category, LinkShowType, Links, Tag
from comments.forms import CommentForm
from djangoblog.plugin_manage import hooks
from djangoblog.plugin_manage.hook_constants import ARTICLE_CONTENT_HOOK_NAME
from djangoblog.utils import cache, get_blog_setting, get_sha256
from comments.forms import CommentForm # 评论表单
from djangoblog.plugin_manage import hooks # 插件钩子
from djangoblog.plugin_manage.hook_constants import ARTICLE_CONTENT_HOOK_NAME # 文章内容钩子常量
from djangoblog.utils import cache, get_blog_setting, get_sha256 # 缓存、配置和加密工具
# 创建当前模块的日志记录器
logger = logging.getLogger(__name__)
class ArticleListView(ListView):
# template_name属性用于指定使用哪个模板进行渲染
"""
文章列表基类视图
封装文章列表页的通用逻辑分页缓存上下文处理
被首页分类标签作者等列表页继承
"""
# 模板路径:所有文章列表页共用此模板
template_name = 'blog/article_index.html'
# context_object_name属性用于给上下文变量取名在模板中使用该名字
# 上下文变量名:模板中用{{ article_list }}访问列表数据
context_object_name = 'article_list'
# 页面类型,分类目录或标签列表等
# 页面类型描述(如"分类目录归档"),子类需重写
page_type = ''
# 分页大小:从配置中获取
paginate_by = settings.PAGINATE_BY
# 分页参数名URL中页码的参数名如?page=2
page_kwarg = 'page'
# 友情链接显示类型默认为列表页L
link_type = LinkShowType.L
def get_view_cache_key(self):
"""获取视图缓存的key未实际使用预留扩展"""
return self.request.get['pages']
@property
def page_number(self):
"""获取当前页码从URL参数或kwargs中提取"""
page_kwarg = self.page_kwarg
page = self.kwargs.get(
page_kwarg) or self.request.GET.get(page_kwarg) or 1
# 优先从URL路径参数获取再从GET参数获取默认1
page = self.kwargs.get(page_kwarg) or self.request.GET.get(page_kwarg) or 1
return page
def get_queryset_cache_key(self):
"""
子类重写.获得queryset的缓存key
抽象方法获取查询集的缓存key
子类必须实现用于区分不同页面的缓存
"""
raise NotImplementedError()
def get_queryset_data(self):
"""
子类重写.获取queryset的数据
抽象方法获取查询集数据
子类必须实现定义具体的文章筛选逻辑
"""
raise NotImplementedError()
def get_queryset_from_cache(self, cache_key):
'''
缓存页面数据
:param cache_key: 缓存key
:return:
'''
"""
从缓存获取或生成查询集数据
:param cache_key: 缓存唯一标识
:return: 文章查询集
"""
# 尝试从缓存获取
value = cache.get(cache_key)
if value:
logger.info('get view cache.key:{key}'.format(key=cache_key))
logger.info(f'从缓存获取数据key: {cache_key}')
return value
else:
# 缓存未命中,执行查询并缓存
article_list = self.get_queryset_data()
cache.set(cache_key, article_list)
logger.info('set view cache.key:{key}'.format(key=cache_key))
logger.info(f'设置缓存key: {cache_key}')
return article_list
def get_queryset(self):
'''
重写默认从缓存获取数据
:return:
'''
key = self.get_queryset_cache_key()
value = self.get_queryset_from_cache(key)
return value
"""
重写父类方法从缓存获取查询集
优化性能减少数据库查询
"""
cache_key = self.get_queryset_cache_key()
return self.get_queryset_from_cache(cache_key)
def get_context_data(self, **kwargs):
"""
扩展上下文数据添加友情链接显示类型
"""
kwargs['linktype'] = self.link_type
return super(ArticleListView, self).get_context_data(**kwargs)
return super().get_context_data(** kwargs)
class IndexView(ArticleListView):
'''
首页
'''
# 友情链接类型
"""
首页视图
继承文章列表基类展示所有已发布的文章
"""
# 友情链接显示类型首页I
link_type = LinkShowType.I
def get_queryset_data(self):
article_list = Article.objects.filter(type='a', status='p')
return article_list
"""获取首页文章列表已发布的普通文章type='a'"""
return Article.objects.filter(type='a', status='p')
def get_queryset_cache_key(self):
cache_key = 'index_{page}'.format(page=self.page_number)
return cache_key
"""生成首页缓存key包含页码"""
return f'index_{self.page_number}'
class ArticleDetailView(DetailView):
'''
文章详情页面
'''
template_name = 'blog/article_detail.html'
model = Article
pk_url_kwarg = 'article_id'
context_object_name = "article"
"""
文章详情页视图
展示单篇文章的详细内容评论等
"""
template_name = 'blog/article_detail.html' # 详情页模板
model = Article # 关联的模型
pk_url_kwarg = 'article_id' # URL中主键的参数名
context_object_name = "article" # 模板中文章对象的变量名
def get_context_data(self, **kwargs):
"""
构建详情页上下文数据
- 评论表单
- 评论分页
- 上下篇文章
- 插件钩子处理
"""
# 初始化评论表单
comment_form = CommentForm()
# 获取当前文章的所有有效评论
article_comments = self.object.comment_list()
# 筛选顶级评论(无父评论)
parent_comments = article_comments.filter(parent_comment=None)
# 获取博客配置(评论分页大小)
blog_setting = get_blog_setting()
# 初始化评论分页器
paginator = Paginator(parent_comments, blog_setting.article_comment_count)
# 处理评论页码参数
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
page = max(1, min(page, paginator.num_pages)) # 限制页码范围
# 获取当前页的评论
p_comments = paginator.page(page)
# 生成上下页评论的URL
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
if next_page:
kwargs[
'comment_next_page_url'] = self.object.get_absolute_url() + f'?comment_page={next_page}#commentlist-container'
kwargs['comment_next_page_url'] = f'{self.object.get_absolute_url()}?comment_page={next_page}#commentlist-container'
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
kwargs['comment_prev_page_url'] = f'{self.object.get_absolute_url()}?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 # 评论总数
# 上下篇文章
kwargs['next_article'] = self.object.next_article
kwargs['prev_article'] = self.object.prev_article
context = super(ArticleDetailView, self).get_context_data(**kwargs)
# 调用父类方法获取基础上下文
context = super().get_context_data(**kwargs)
# 获取当前文章对象
article = self.object
# Action Hook, 通知插件"文章详情已获取"
# 执行插件动作钩子:通知插件"文章详情已获取"
hooks.run_action('after_article_body_get', article=article, request=self.request)
# # Filter Hook, 允许插件修改文章正文
article.body = hooks.apply_filters(ARTICLE_CONTENT_HOOK_NAME, article.body, article=article,
request=self.request)
# 执行插件过滤钩子:允许插件修改文章正文(如添加水印、解析特殊标签等)
article.body = hooks.apply_filters(
ARTICLE_CONTENT_HOOK_NAME,
article.body,
article=article,
request=self.request
)
return context
class CategoryDetailView(ArticleListView):
'''
分类目录列表
'''
page_type = "分类目录归档"
"""
分类详情页视图
展示指定分类及子分类下的所有文章
"""
page_type = "分类目录归档" # 页面类型描述
def get_queryset_data(self):
"""
获取分类下的文章列表
1. 根据URL中的分类slug获取分类对象
2. 包含所有子分类的文章
3. 仅展示已发布状态
"""
slug = self.kwargs['category_name']
category = get_object_or_404(Category, slug=slug)
category = get_object_or_404(Category, slug=slug) # 获取分类不存在则404
categoryname = category.name
self.categoryname = categoryname
categorynames = list(
map(lambda c: c.name, category.get_sub_categorys()))
article_list = Article.objects.filter(
category__name__in=categorynames, status='p')
return article_list
# 记录分类名称(用于上下文)
self.categoryname = category.name
# 获取当前分类及所有子分类的名称列表
categorynames = [c.name for c in category.get_sub_categorys()]
# 筛选属于这些分类且已发布的文章
return Article.objects.filter(category__name__in=categorynames, status='p')
def get_queryset_cache_key(self):
"""生成分类页面的缓存key包含分类名和页码"""
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
self.categoryname = category.name
return f'category_list_{self.categoryname}_{self.page_number}'
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)
categoryname = self.categoryname.split('/')[-1]
except:
categoryname = self.categoryname
kwargs['page_type'] = self.page_type
kwargs['tag_name'] = categoryname # 模板中统一用tag_name显示当前分类/标签/作者名
return super().get_context_data(** kwargs)
class AuthorDetailView(ArticleListView):
'''
作者详情页
'''
page_type = '作者文章归档'
"""
作者详情页视图
展示指定作者发布的所有文章
"""
page_type = '作者文章归档' # 页面类型描述
def get_queryset_cache_key(self):
from uuslug import slugify
"""生成作者页面的缓存key包含作者名和页码"""
from uuslug import slugify # 确保作者名URL友好
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
return f'author_{author_name}_{self.page_number}'
def get_queryset_data(self):
"""获取指定作者的已发布文章"""
author_name = self.kwargs['author_name']
article_list = Article.objects.filter(
author__username=author_name, type='a', status='p')
return article_list
return Article.objects.filter(author__username=author_name, type='a', status='p')
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)
"""扩展上下文:添加页面类型和作者名"""
kwargs['page_type'] = self.page_type
kwargs['tag_name'] = self.kwargs['author_name']
return super().get_context_data(** kwargs)
class TagDetailView(ArticleListView):
'''
标签列表页面
'''
page_type = '分类标签归档'
"""
标签详情页视图
展示指定标签关联的所有文章
"""
page_type = '分类标签归档' # 页面类型描述
def get_queryset_data(self):
"""获取指定标签的已发布文章"""
slug = self.kwargs['tag_name']
tag = get_object_or_404(Tag, slug=slug)
tag_name = tag.name
self.name = tag_name
article_list = Article.objects.filter(
tags__name=tag_name, type='a', status='p')
return article_list
tag = get_object_or_404(Tag, slug=slug) # 获取标签不存在则404
self.name = tag.name # 记录标签名
return Article.objects.filter(tags__name=self.name, type='a', status='p')
def get_queryset_cache_key(self):
"""生成标签页面的缓存key包含标签名和页码"""
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
self.name = tag.name
return f'tag_{self.name}_{self.page_number}'
def get_context_data(self, **kwargs):
# tag_name = self.kwargs['tag_name']
tag_name = self.name
kwargs['page_type'] = TagDetailView.page_type
kwargs['tag_name'] = tag_name
return super(TagDetailView, self).get_context_data(**kwargs)
"""扩展上下文:添加页面类型和标签名"""
kwargs['page_type'] = self.page_type
kwargs['tag_name'] = self.name
return super().get_context_data(** kwargs)
class ArchivesView(ArticleListView):
'''
文章归档页面
'''
"""
文章归档页面视图
展示所有已发布文章的归档列表按时间分组
"""
page_type = '文章归档'
paginate_by = None
page_kwarg = None
template_name = 'blog/article_archives.html'
paginate_by = None # 归档页不分页
page_kwarg = None # 无需页码参数
template_name = 'blog/article_archives.html' # 归档页专用模板
def get_queryset_data(self):
"""获取所有已发布文章(用于归档)"""
return Article.objects.filter(status='p').all()
def get_queryset_cache_key(self):
cache_key = 'archives'
return cache_key
"""归档页缓存key固定值因不分页"""
return 'archives'
class LinkListView(ListView):
model = Links
template_name = 'blog/links_list.html'
"""
友情链接页面视图
展示所有启用的友情链接
"""
model = Links # 关联链接模型
template_name = 'blog/links_list.html' # 链接页模板
def get_queryset(self):
"""仅获取启用的友情链接"""
return Links.objects.filter(is_enable=True)
class EsSearchView(SearchView):
"""
搜索视图基于Haystack
处理全文搜索请求并返回结果
"""
def get_context(self):
"""构建搜索结果页面的上下文数据"""
# 构建分页器和当前页数据
paginator, page = self.build_page()
context = {
"query": self.query,
"form": self.form,
"page": page,
"paginator": paginator,
"suggestion": None,
"query": self.query, # 搜索关键词
"form": self.form, # 搜索表单
"page": page, # 当前页结果
"paginator": paginator, # 分页器
"suggestion": 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())
# 添加额外上下文
context.update(self.extra_context())
return context
@csrf_exempt
@csrf_exempt # 豁免CSRF验证用于外部调用上传
def fileupload(request):
"""
该方法需自己写调用端来上传图片该方法仅提供图床功能
:param request:
:return:
文件上传接口图床功能
仅允许POST请求且需验证签名
"""
if request.method == 'POST':
# 获取签名参数
sign = request.GET.get('sign', None)
if not sign:
return HttpResponseForbidden()
if not sign == get_sha256(get_sha256(settings.SECRET_KEY)):
return HttpResponseForbidden()
return HttpResponseForbidden() # 无签名则禁止
# 验证签名双重SHA256加密基于SECRET_KEY
if sign != get_sha256(get_sha256(settings.SECRET_KEY)):
return HttpResponseForbidden() # 签名无效则禁止
# 存储上传文件的URL
response = []
# 处理每个上传的文件
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
base_dir = os.path.join(settings.STATICFILES, "files" if not isimage else "image", timestr)
# 检查是否为图片
fname = str(filename)
isimage = any(ext in fname.lower() for ext in imgextensions)
# 确定存储目录(图片和普通文件分开)
base_dir = os.path.join(
settings.STATICFILES,
"image" if isimage else "files",
timestr
)
# 确保目录存在
if not os.path.exists(base_dir):
os.makedirs(base_dir)
savepath = os.path.normpath(os.path.join(base_dir, f"{uuid.uuid4().hex}{os.path.splitext(filename)[-1]}"))
# 生成唯一文件名UUID+原扩展名)
file_ext = os.path.splitext(filename)[-1]
savepath = os.path.normpath(
os.path.join(base_dir, f"{uuid.uuid4().hex}{file_ext}")
)
# 安全检查:防止路径穿越
if not savepath.startswith(base_dir):
return HttpResponse("only for post")
return HttpResponse("Invalid path")
# 保存文件
with open(savepath, 'wb+') as wfile:
for chunk in request.FILES[filename].chunks():
wfile.write(chunk)
# 压缩图片(如果是图片文件)
if isimage:
from PIL import Image
image = Image.open(savepath)
image.save(savepath, quality=20, optimize=True)
try:
with Image.open(savepath) as image:
# 优化图片质量20%质量,启用优化)
image.save(savepath, quality=20, optimize=True)
except Exception as e:
logger.error(f"图片压缩失败: {e}")
# 生成文件的访问URL
url = static(savepath)
response.append(url)
return HttpResponse(response)
# 返回所有上传文件的URL
return HttpResponse(response)
else:
# 仅允许POST请求
return HttpResponse("only for post")
def page_not_found_view(
request,
exception,
template_name='blog/error_page.html'):
def page_not_found_view(request, exception, template_name='blog/error_page.html'):
"""
404错误页面视图
处理页面未找到的情况
"""
if exception:
logger.error(exception)
url = request.get_full_path()
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)
logger.error(exception) # 记录错误详情
url = request.get_full_path() # 获取请求的URL
return render(
request,
template_name,
{
'message': _('Sorry, the page you requested is not found. Please click the home page to see others.'),
'statuscode': '404'
},
status=404
)
def server_error_view(request, template_name='blog/error_page.html'):
return render(request,
template_name,
{'message': _('Sorry, the server is busy, please click the home page to see other?'),
'statuscode': '500'},
status=500)
"""
500错误页面视图
处理服务器内部错误
"""
return render(
request,
template_name,
{
'message': _('Sorry, the server is busy. Please click the home page to see others.'),
'statuscode': '500'
},
status=500
)
def permission_denied_view(
request,
exception,
template_name='blog/error_page.html'):
def permission_denied_view(request, exception, template_name='blog/error_page.html'):
"""
403错误页面视图
处理权限不足的情况
"""
if exception:
logger.error(exception)
logger.error(exception) # 记录错误详情
return render(
request, template_name, {
'message': _('Sorry, you do not have permission to access this page?'),
'statuscode': '403'}, status=403)
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()
return HttpResponse('ok')
return HttpResponse('ok')
Loading…
Cancel
Save