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): """ mk: 文章列表视图基类,继承自Django的ListView Attributes: template_name (str): 模板文件路径 context_object_name (str): 上下文对象名称 page_type (str): 页面类型标识 paginate_by (int): 每页显示的文章数量 page_kwarg (str): 分页参数名称 link_type (LinkShowType): 链接显示类型 """ # mk:template_name属性用于指定使用哪个模板进行渲染 template_name = 'blog/article_index.html' # mk:context_object_name属性用于给上下文变量取名(在模板中使用该名字) context_object_name = 'article_list' # mk:页面类型,分类目录或标签列表等 page_type = '' paginate_by = settings.PAGINATE_BY page_kwarg = 'page' link_type = LinkShowType.L def get_view_cache_key(self): """ mk: 获取视图缓存键 Returns: str: 缓存键值 """ return self.request.get['pages'] @property def page_number(self): """ mk: 获取当前页码 Returns: int: 当前页码 """ 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): """ mk: 子类重写.获得queryset的缓存key Raises: NotImplementedError: 子类必须实现此方法 """ raise NotImplementedError() def get_queryset_data(self): """ mk: 子类重写.获取queryset的数据 Raises: NotImplementedError: 子类必须实现此方法 """ raise NotImplementedError() def get_queryset_from_cache(self, cache_key): """ mk: 从缓存获取查询结果集 Args: cache_key (str): 缓存键 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): """ mk: 重写默认方法,从缓存获取数据 Returns: QuerySet: 查询结果集 """ key = self.get_queryset_cache_key() value = self.get_queryset_from_cache(key) return value def get_context_data(self, **kwargs): """ mk: 获取上下文数据 Args: **kwargs: 额外的关键字参数 Returns: dict: 上下文字典 """ kwargs['linktype'] = self.link_type return super(ArticleListView, self).get_context_data(**kwargs) class IndexView(ArticleListView): """ mk: 首页视图类,显示所有已发布的文章列表 """ # 友情链接类型 link_type = LinkShowType.I def get_queryset_data(self): """ mk: 获取首页文章数据 Returns: QuerySet: 已发布文章的查询集 """ article_list = Article.objects.filter(type='a', status='p') return article_list def get_queryset_cache_key(self): """ mk: 获取首页缓存键 Returns: str: 首页缓存键 """ cache_key = 'index_{page}'.format(page=self.page_number) return cache_key class ArticleDetailView(DetailView): """ mk: 文章详情页面视图类 Attributes: template_name (str): 模板文件路径 model: 数据模型类 pk_url_kwarg (str): URL中的主键参数名 context_object_name (str): 上下文对象名称 """ template_name = 'blog/article_detail.html' model = Article pk_url_kwarg = 'article_id' context_object_name = "article" def get_context_data(self, **kwargs): """ mk: 获取文章详情页上下文数据,包括评论分页、前后文章等信息 Args: **kwargs: 额外的关键字参数 Returns: dict: 包含文章详情和相关数据的上下文字典 """ 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 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 # mk:触发文章详情加载钩子,让插件可以添加额外的上下文数据 from djangoblog.plugin_manage.hook_constants import ARTICLE_DETAIL_LOAD hooks.run_action(ARTICLE_DETAIL_LOAD, article=article, context=context, request=self.request) # mk:Action Hook, 通知插件"文章详情已获取" hooks.run_action('after_article_body_get', article=article, request=self.request) return context class CategoryDetailView(ArticleListView): """ mk: 分类目录列表视图类,显示指定分类下的文章列表 """ page_type = "分类目录归档" def get_queryset_data(self): """ mk: 根据分类名称获取该分类及其子分类下的所有文章 Returns: QuerySet: 指定分类下的文章查询集 """ 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): """ mk: 获取分类页面缓存键 Returns: str: 分类页面缓存键 """ 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): """ mk: 获取分类页面上下文数据 Args: **kwargs: 额外的关键字参数 Returns: dict: 包含分类信息的上下文字典 """ 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): """ mk: 作者详情页视图类,显示指定作者的文章列表 """ page_type = '作者文章归档' def get_queryset_cache_key(self): """ mk: 获取作者页面缓存键 Returns: str: 作者页面缓存键 """ 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 def get_queryset_data(self): """ mk: 根据作者用户名获取该作者的所有文章 Returns: QuerySet: 指定作者的文章查询集 """ 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): """ mk: 获取作者页面上下文数据 Args: **kwargs: 额外的关键字参数 Returns: dict: 包含作者信息的上下文字典 """ 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): """ mk: 标签列表页面视图类,显示指定标签下的文章列表 """ page_type = '分类标签归档' def get_queryset_data(self): """ mk: 根据标签名称获取该标签下的所有文章 Returns: QuerySet: 指定标签下的文章查询集 """ 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): """ mk: 获取标签页面缓存键 Returns: str: 标签页面缓存键 """ 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): """ mk: 获取标签页面上下文数据 Args: **kwargs: 额外的关键字参数 Returns: dict: 包含标签信息的上下文字典 """ # 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) class ArchivesView(ArticleListView): """ mk: 文章归档页面视图类,显示所有文章的时间归档 """ page_type = '文章归档' paginate_by = None page_kwarg = None template_name = 'blog/article_archives.html' def get_queryset_data(self): """ mk: 获取所有已发布文章的数据 Returns: QuerySet: 所有已发布文章的查询集 """ return Article.objects.filter(status='p').all() def get_queryset_cache_key(self): """ mk: 获取归档页面缓存键 Returns: str: 归档页面缓存键 """ cache_key = 'archives' return cache_key class LinkListView(ListView): """ mk: 友情链接列表视图类 Attributes: model: 链接数据模型 template_name (str): 模板文件路径 """ model = Links template_name = 'blog/links_list.html' def get_queryset(self): """ mk: 获取启用的友情链接查询集 Returns: QuerySet: 启用的友情链接查询集 """ return Links.objects.filter(is_enable=True) class EsSearchView(SearchView): """ mk: Elasticsearch搜索视图类,继承自haystack的SearchView """ def get_context(self): """ mk: 获取搜索结果上下文数据 Returns: dict: 包含搜索结果的上下文字典 """ 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 def fileupload(request): """ mk: 文件上传处理函数,提供图床功能 Args: request (HttpRequest): HTTP请求对象 Returns: HttpResponse: 包含上传文件URL的响应对象 """ 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) url = static(savepath) response.append(url) return HttpResponse(response) else: return HttpResponse("only for post") def page_not_found_view( request, exception, template_name='blog/error_page.html'): """ mk: 404页面未找到错误处理函数 Args: request (HttpRequest): HTTP请求对象 exception (Exception): 异常对象 template_name (str): 错误页面模板名称 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'): """ mk: 500服务器内部错误处理函数 Args: request (HttpRequest): HTTP请求对象 template_name (str): 错误页面模板名称 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'): """ mk: 403权限拒绝错误处理函数 Args: request (HttpRequest): HTTP请求对象 exception (Exception): 异常对象 template_name (str): 错误页面模板名称 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): """ mk: 清除缓存视图函数 Args: request (HttpRequest): HTTP请求对象 Returns: HttpResponse: 返回'ok'表示清除成功 """ cache.clear() return HttpResponse('ok')