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.
ReviewAndAnalyzeOpenSourceS.../src/DjangoBlog/blog/views.py

498 lines
18 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.

# bjy: 导入日志、操作系统和UUID模块
import logging
import os
import uuid
# bjy: 从Django中导入设置、分页器、HTTP响应、快捷函数、静态文件、时区、国际化、CSRF豁免和基于类的视图
from django.conf import settings
from django.contrib.auth.decorators import login_required
from django.core.paginator import Paginator
from django.http import HttpResponse, HttpResponseForbidden, JsonResponse
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.decorators import method_decorator
from django.utils.translation import gettext_lazy as _
from django.views import View
from django.views.decorators.csrf import csrf_exempt
from django.views.generic.detail import DetailView
from django.views.generic.list import ListView
# bjy: 从Haystack中导入搜索视图
from haystack.views import SearchView
# bjy: 从项目中导入博客模型、评论表单、插件管理器和工具函数
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
# bjy: 获取一个名为__name__的logger实例用于记录日志
logger = logging.getLogger(__name__)
# bjy: 定义一个基于类的文章列表视图,作为其他列表视图的基类
class ArticleListView(ListView):
# bjy: 指定渲染的模板文件
template_name = 'blog/article_index.html'
# bjy: 指定在模板中使用的上下文变量名
context_object_name = 'article_list'
# bjy: 页面类型,用于在模板中显示不同的标题
page_type = ''
# bjy: 每页显示的文章数量,从设置中获取
paginate_by = settings.PAGINATE_BY
# bjy: URL中分页参数的名称
page_kwarg = 'page'
# bjy: 友情链接的显示类型,默认为列表页
link_type = LinkShowType.L
# bjy: 获取视图的缓存键(此方法未使用)
def get_view_cache_key(self):
return self.request.get['pages']
# bjy: 属性,用于获取当前页码
@property
def page_number(self):
page_kwarg = self.page_kwarg
# bjy: 从URL参数或GET参数中获取页码默认为1
page = self.kwargs.get(
page_kwarg) or self.request.GET.get(page_kwarg) or 1
return page
# bjy: 抽象方法要求子类实现用于获取queryset的缓存键
def get_queryset_cache_key(self):
"""
子类重写.获得queryset的缓存key
"""
raise NotImplementedError()
# bjy: 抽象方法,要求子类实现,用于获取实际的数据集
def get_queryset_data(self):
"""
子类重写.获取queryset的数据
"""
raise NotImplementedError()
# bjy: 从缓存中获取数据集,如果缓存不存在则查询数据库并存入缓存
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:
# bjy: 调用子类实现的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
# bjy: 重写父类方法从缓存中获取queryset
def get_queryset(self):
'''
重写默认,从缓存获取数据
:return:
'''
key = self.get_queryset_cache_key()
value = self.get_queryset_from_cache(key)
return value
# bjy: 重写父类方法,向上下文中添加链接类型
def get_context_data(self, **kwargs):
kwargs['linktype'] = self.link_type
return super(ArticleListView, self).get_context_data(**kwargs)
# bjy: 首页视图继承自ArticleListView
class IndexView(ArticleListView):
'''
首页
'''
# 友情链接类型
link_type = LinkShowType.I
# bjy: 实现父类的抽象方法,获取首页的文章数据
def get_queryset_data(self):
article_list = Article.objects.filter(type='a', status='p')
return article_list
# bjy: 实现父类的抽象方法,生成首页的缓存键
def get_queryset_cache_key(self):
cache_key = 'index_{page}'.format(page=self.page_number)
return cache_key
# bjy: 文章详情页视图
class ArticleDetailView(DetailView):
'''
文章详情页面
'''
template_name = 'blog/article_detail.html'
model = Article
pk_url_kwarg = 'article_id'
context_object_name = "article"
# bjy: 重写父类方法,向上下文中添加额外的数据
def get_context_data(self, **kwargs):
# bjy: 创建评论表单实例
comment_form = CommentForm()
# bjy: 获取文章的所有评论
article_comments = self.object.comment_list()
# bjy: 筛选出父评论(顶级评论)
parent_comments = article_comments.filter(parent_comment=None)
# bjy: 获取博客设置
blog_setting = get_blog_setting()
# bjy: 对父评论进行分页
paginator = Paginator(parent_comments, blog_setting.article_comment_count)
# bjy: 从GET参数中获取评论页码
page = self.request.GET.get('comment_page', '1')
# bjy: 校验页码是否为有效数字
if not page.isnumeric():
page = 1
else:
page = int(page)
if page < 1:
page = 1
if page > paginator.num_pages:
page = paginator.num_pages
# bjy: 获取当前页的评论对象
p_comments = paginator.page(page)
# bjy: 获取下一页和上一页的页码
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
# bjy: 如果存在下一页则构建下一页的URL
if next_page:
kwargs[
'comment_next_page_url'] = self.object.get_absolute_url() + f'?comment_page={next_page}#commentlist-container'
# bjy: 如果存在上一页则构建上一页的URL
if prev_page:
kwargs[
'comment_prev_page_url'] = self.object.get_absolute_url() + f'?comment_page={prev_page}#commentlist-container'
# bjy: 将评论表单和评论数据添加到上下文
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
# bjy: 添加上一篇和下一篇文章
kwargs['next_article'] = self.object.next_article
kwargs['prev_article'] = self.object.prev_article
# bjy: 调用父类方法获取基础上下文
context = super(ArticleDetailView, self).get_context_data(**kwargs)
article = self.object
# bjy: 触发文章详情加载钩子,让插件可以添加额外的上下文数据
from djangoblog.plugin_manage.hook_constants import ARTICLE_DETAIL_LOAD
hooks.run_action(ARTICLE_DETAIL_LOAD, article=article, context=context, request=self.request)
# bjy: Action Hook, 通知插件"文章详情已获取"
hooks.run_action('after_article_body_get', article=article, request=self.request)
return context
# bjy: 分类详情页视图
class CategoryDetailView(ArticleListView):
'''
分类目录列表
'''
page_type = "分类目录归档"
# bjy: 实现父类的抽象方法,获取分类下的文章数据
def get_queryset_data(self):
slug = self.kwargs['category_name']
# bjy: 根据slug获取分类对象如果不存在则返回404
category = get_object_or_404(Category, slug=slug)
categoryname = category.name
self.categoryname = categoryname
# bjy: 获取该分类及其所有子分类的名称列表
categorynames = list(
map(lambda c: c.name, category.get_sub_categorys()))
# bjy: 筛选出属于这些分类的所有已发布文章
article_list = Article.objects.filter(
category__name__in=categorynames, status='p')
return article_list
# bjy: 实现父类的抽象方法,生成分类页的缓存键
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
# bjy: 重写父类方法,向上下文中添加分类名称
def get_context_data(self, **kwargs):
categoryname = self.categoryname
try:
# bjy: 处理多级分类的情况,只取最后一部分
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)
# bjy: 作者详情页视图
class AuthorDetailView(ArticleListView):
'''
作者详情页
'''
page_type = '作者文章归档'
# bjy: 实现父类的抽象方法,生成作者页的缓存键
def get_queryset_cache_key(self):
from uuslug import slugify
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
# bjy: 实现父类的抽象方法,获取作者的文章数据
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
# bjy: 重写父类方法,向上下文中添加作者名称
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)
# bjy: 标签详情页视图
class TagDetailView(ArticleListView):
'''
标签列表页面
'''
page_type = '分类标签归档'
# bjy: 实现父类的抽象方法,获取标签下的文章数据
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
# bjy: 实现父类的抽象方法,生成标签页的缓存键
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
# bjy: 重写父类方法,向上下文中添加标签名称
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)
# bjy: 文章归档页视图
class ArchivesView(ArticleListView):
'''
文章归档页面
'''
page_type = '文章归档'
# bjy: 归档页不分页
paginate_by = None
page_kwarg = None
template_name = 'blog/article_archives.html'
# bjy: 实现父类的抽象方法,获取所有已发布文章
def get_queryset_data(self):
return Article.objects.filter(status='p').all()
# bjy: 实现父类的抽象方法,生成归档页的缓存键
def get_queryset_cache_key(self):
cache_key = 'archives'
return cache_key
# bjy: 友情链接页视图
class LinkListView(ListView):
model = Links
template_name = 'blog/links_list.html'
# bjy: 重写queryset只获取已启用的链接
def get_queryset(self):
return Links.objects.filter(is_enable=True)
# bjy: 自定义的Elasticsearch搜索视图
class EsSearchView(SearchView):
# bjy: 重写get_context方法自定义搜索结果的上下文
def get_context(self):
paginator, page = self.build_page()
context = {
"query": self.query,
"form": self.form,
"page": page,
"paginator": paginator,
"suggestion": None,
}
# bjy: 如果启用了拼写建议,则添加到上下文
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
class LikeArticle(View):
"""
处理文章点赞和取消点赞
"""
@method_decorator(login_required) # 确保只有登录用户才能点赞
def post(self, request):
try:
user = request.user
article_id = request.POST.get('article_id') # 获取文章ID
article = Article.objects.get(id=article_id) # 获取文章对象
# 检查当前用户是否已经为这篇文章点过赞
if article.users_like.filter(id=user.id).exists():
# 如果点过赞,则取消点赞 (从多对多关系中移除)
article.users_like.remove(user)
action_type = 0 # 0代表取消点赞
else:
# 如果没点过赞,则添加点赞 (添加到多对多关系)
article.users_like.add(user)
action_type = 1 # 1代表点赞
# 获取更新后的点赞总数
like_count = article.users_like.count()
# 返回JSON数据给前端
return JsonResponse({
'state': 200,
'type': action_type,
'like_sum': like_count
})
except Article.DoesNotExist:
return JsonResponse({'state': 400, 'data': '文章不存在'})
except Exception as e:
return JsonResponse({'state': 500, 'data': f'服务器错误: {e}'})
# bjy: 文件上传视图使用csrf_exempt豁免CSRF验证
@csrf_exempt
def fileupload(request):
"""
该方法需自己写调用端来上传图片,该方法仅提供图床功能
:param request:
:return:
"""
if request.method == 'POST':
# bjy: 从GET参数中获取签名
sign = request.GET.get('sign', None)
if not sign:
return HttpResponseForbidden()
# bjy: 验证签名是否正确
if not sign == get_sha256(get_sha256(settings.SECRET_KEY)):
return HttpResponseForbidden()
response = []
# bjy: 遍历所有上传的文件
for filename in request.FILES:
# bjy: 按年/月/日创建目录
timestr = timezone.now().strftime('%Y/%m/%d')
imgextensions = ['jpg', 'png', 'jpeg', 'bmp']
fname = u''.join(str(filename))
# bjy: 判断文件是否为图片
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)
# bjy: 如果目录不存在则创建
if not os.path.exists(base_dir):
os.makedirs(base_dir)
# bjy: 生成唯一的文件名并拼接保存路径
savepath = os.path.normpath(os.path.join(base_dir, f"{uuid.uuid4().hex}{os.path.splitext(filename)[-1]}"))
# bjy: 安全检查,防止路径遍历攻击
if not savepath.startswith(base_dir):
return HttpResponse("only for post")
# bjy: 将文件内容写入磁盘
with open(savepath, 'wb+') as wfile:
for chunk in request.FILES[filename].chunks():
wfile.write(chunk)
# bjy: 如果是图片,则进行压缩优化
if isimage:
from PIL import Image
image = Image.open(savepath)
image.save(savepath, quality=20, optimize=True)
# bjy: 生成文件的静态URL
url = static(savepath)
response.append(url)
# bjy: 返回包含所有文件URL的响应
return HttpResponse(response)
else:
# bjy: 非POST请求返回错误信息
return HttpResponse("only for post")
# bjy: 自定义404错误处理视图
def page_not_found_view(
request,
exception,
template_name='blog/error_page.html'):
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)
# bjy: 自定义500服务器错误处理视图
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)
# bjy: 自定义403权限拒绝错误处理视图
def permission_denied_view(
request,
exception,
template_name='blog/error_page.html'):
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)
# bjy: 清除缓存的视图
def clean_cache_view(request):
cache.clear()
return HttpResponse('ok')