|
|
|
|
@ -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')
|