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.
DjangoBlog-Maintenance-Anal.../src/views.py

501 lines
16 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.

import logging
import os
import uuid
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 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
logger = logging.getLogger(__name__)
class ArticleListView(ListView):
"""
文章列表基类视图
提供通用的文章列表功能和缓存机制
所有文章列表视图都应该继承此类
"""
# template_name属性用于指定使用哪个模板进行渲染
template_name = 'blog/article_index.html'
# context_object_name属性用于给上下文变量取名在模板中使用该名字
context_object_name = 'article_list'
# 页面类型,分类目录或标签列表等
page_type = ''
paginate_by = settings.PAGINATE_BY # 每页显示的文章数量
page_kwarg = 'page' # URL中页码参数的名称
link_type = LinkShowType.L # 友情链接显示类型
def get_view_cache_key(self):
"""获取视图缓存键 - 需要子类实现"""
return self.request.get['pages']
@property
def page_number(self):
"""获取当前页码"""
page_kwarg = self.page_kwarg
page = self.kwargs.get(
page_kwarg) or self.request.GET.get(page_kwarg) or 1
return page
def get_queryset_cache_key(self):
"""
获取查询集缓存键
子类必须重写此方法
"""
raise NotImplementedError()
def get_queryset_data(self):
"""
获取查询集数据
子类必须重写此方法
"""
raise NotImplementedError()
def get_queryset_from_cache(self, cache_key):
"""
从缓存获取查询集数据
Args:
cache_key: 缓存键
Returns:
QuerySet: 文章查询集
"""
value = cache.get(cache_key)
if value:
logger.info('get view cache.key:{key}'.format(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))
return article_list
def get_queryset(self):
"""
获取查询集 - 从缓存获取数据
Returns:
QuerySet: 文章查询集
"""
key = self.get_queryset_cache_key()
value = self.get_queryset_from_cache(key)
return value
def get_context_data(self, **kwargs):
"""添加上下文数据"""
kwargs['linktype'] = self.link_type
return super(ArticleListView, self).get_context_data(**kwargs)
class IndexView(ArticleListView):
"""
首页视图
显示最新的文章列表
"""
# 友情链接类型 - 首页显示
link_type = LinkShowType.I
def get_queryset_data(self):
"""获取首页文章数据 - 只获取已发布的普通文章"""
article_list = Article.objects.filter(type='a', status='p')
return article_list
def get_queryset_cache_key(self):
"""获取首页缓存键 - 基于页码"""
cache_key = 'index_{page}'.format(page=self.page_number)
return cache_key
class ArticleDetailView(DetailView):
"""
文章详情页面视图
显示单篇文章的详细内容和评论
"""
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
# 获取当前页的评论
p_comments = paginator.page(page)
# 计算下一页和上一页
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
if next_page:
kwargs[
'comment_next_page_url'] = self.object.get_absolute_url() + f'?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['next_article'] = self.object.next_article
kwargs['prev_article'] = self.object.prev_article
# 调用父类方法获取基础上下文
context = super(ArticleDetailView, self).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)
return context
class CategoryDetailView(ArticleListView):
"""
分类目录列表视图
显示指定分类下的所有文章(包括子分类)
"""
page_type = "分类目录归档"
def get_queryset_data(self):
"""获取分类文章数据 - 包括所有子分类的文章"""
slug = self.kwargs['category_name']
category = get_object_or_404(Category, slug=slug)
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
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
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)
class AuthorDetailView(ArticleListView):
"""
作者详情页视图
显示指定作者的所有文章
"""
page_type = '作者文章归档'
def get_queryset_cache_key(self):
"""获取作者页面缓存键 - 基于作者名称和页码"""
from uuslug import slugify
author_name = slugify(self.kwargs['author_name']) # 使用slugify处理作者名
cache_key = 'author_{author_name}_{page}'.format(
author_name=author_name, page=self.page_number)
return cache_key
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
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)
class TagDetailView(ArticleListView):
"""
标签列表页面视图
显示指定标签下的所有文章
"""
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
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
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)
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()
def get_queryset_cache_key(self):
"""获取归档页面缓存键 - 固定键名"""
cache_key = 'archives'
return cache_key
class LinkListView(ListView):
"""
友情链接列表视图
显示所有启用的友情链接
"""
model = Links # 关联的模型
template_name = 'blog/links_list.html' # 友情链接模板
def get_queryset(self):
"""获取启用的友情链接"""
return Links.objects.filter(is_enable=True)
class EsSearchView(SearchView):
"""
Elasticsearch搜索视图
扩展Haystack的搜索功能
"""
def get_context(self):
"""获取搜索上下文数据"""
paginator, page = self.build_page() # 构建分页
context = {
"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())
return context
@csrf_exempt # 免除CSRF验证用于文件上传
def fileupload(request):
"""
文件上传视图
提供图床功能,支持图片和文件上传
Args:
request: HTTP请求对象
Returns:
HttpResponse: 上传结果
"""
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()
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)
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]}"))
# 安全检查:确保文件保存在指定目录内
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
image = Image.open(savepath)
image.save(savepath, quality=20, optimize=True) # 压缩质量20%
# 生成静态文件URL
url = static(savepath)
response.append(url)
return HttpResponse(response)
else:
return HttpResponse("only for post") # 只支持POST请求
def page_not_found_view(
request,
exception,
template_name='blog/error_page.html'):
"""
404页面未找到视图
Args:
request: 请求对象
exception: 异常信息
template_name: 模板名称
Returns:
HttpResponse: 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)
def server_error_view(request, template_name='blog/error_page.html'):
"""
500服务器错误视图
Args:
request: 请求对象
template_name: 模板名称
Returns:
HttpResponse: 500错误页面
"""
return render(request,
template_name,
{'message': _('Sorry, the server is busy, please click the home page to see other?'),
'statuscode': '500'},
status=500)
def permission_denied_view(
request,
exception,
template_name='blog/error_page.html'):
"""
403权限拒绝视图
Args:
request: 请求对象
exception: 异常信息
template_name: 模板名称
Returns:
HttpResponse: 403错误页面
"""
if exception:
logger.error(exception) # 记录异常日志
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):
"""
清理缓存视图
用于手动清理系统缓存
Args:
request: 请求对象
Returns:
HttpResponse: 清理结果
"""
cache.clear()
return HttpResponse('ok')