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