diff --git a/djangoblog/src/DjangoBlog-master/DjangoBlog-master/blog/views.py b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/blog/views.py index d5dc7ec0..09c7c520 100644 --- a/djangoblog/src/DjangoBlog-master/DjangoBlog-master/blog/views.py +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/blog/views.py @@ -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') \ No newline at end of file