diff --git a/src/DjangoBlog/blog/models.py b/src/DjangoBlog/blog/models.py index b70c658..11cd326 100644 --- a/src/DjangoBlog/blog/models.py +++ b/src/DjangoBlog/blog/models.py @@ -1,5 +1,5 @@ -# 这个文件里的是博客相关的数据模型,定义了博客系统中所有的数据表结构 +#flj 这个文件里的是博客相关的数据模型,定义了博客系统中所有的数据表结构 import logging import re from abc import abstractmethod @@ -20,7 +20,7 @@ logger = logging.getLogger(__name__) -# 友情链接的展示类型选择,用于控制链接在哪些页面显示 +#zxm 友情链接的展示类型选择,用于控制链接在哪些页面显示 class LinkShowType(models.TextChoices): I = ('i', _('index')) # 只在首页显示 L = ('l', _('list')) # 只在列表页显示 @@ -29,22 +29,22 @@ class LinkShowType(models.TextChoices): S = ('s', _('slide')) # 以轮播形式显示 -# 所有模型的基类,包含通用字段,避免重复代码 +#fkc 所有模型的基类,包含通用字段,避免重复代码 class BaseModel(models.Model): - id = models.AutoField(primary_key=True) # 主键,自动递增 - creation_time = models.DateTimeField(_('creation time'), default=now) # 创建时间 - last_modify_time = models.DateTimeField(_('modify time'), default=now) # 最后修改时间 + id = models.AutoField(primary_key=True) #fkc 主键,自动递增 + creation_time = models.DateTimeField(_('creation time'), default=now) #fkc 创建时间 + last_modify_time = models.DateTimeField(_('modify time'), default=now) #fkc 最后修改时间 - # 重写保存方法,自动处理slug字段(用于生成友好的URL) + #fkc 重写保存方法,自动处理slug字段(用于生成友好的URL) def save(self, *args, **kwargs): - # 如果是更新文章浏览量,直接更新数据库,避免触发其他逻辑 + #fkc 如果是更新文章浏览量,直接更新数据库,避免触发其他逻辑 is_update_views = isinstance( self, Article) and 'update_fields' in kwargs and kwargs['update_fields'] == ['views'] if is_update_views: Article.objects.filter(pk=self.pk).update(views=self.views) else: - # 如果有slug字段,自动从标题或名称生成slug + #fkc 如果有slug字段,自动从标题或名称生成slug if 'slug' in self.__dict__: slug = getattr( self, 'title') if 'title' in self.__dict__ else getattr( @@ -53,358 +53,345 @@ class BaseModel(models.Model): super().save(*args, **kwargs) def get_full_url(self): - # 获取完整的URL地址,包含域名 + #fkc 获取完整的URL地址,包含域名 site = get_current_site().domain url = "https://{site}{path}".format(site=site, path=self.get_absolute_url()) return url class Meta: - abstract = True # 这是一个抽象基类,不会创建数据库表 + abstract = True #fkc 这是一个抽象基类,不会创建数据库表 @abstractmethod def get_absolute_url(self): - # 子类必须实现这个方法,返回对象的URL + #fkc 子类必须实现这个方法,返回对象的URL pass +#cll 文章模型,博客的核心内容 class Article(BaseModel): - """文章模型,博客的核心内容""" - # 文章状态选择:草稿或已发布 + #cll 文章状态选择:草稿或已发布 STATUS_CHOICES = ( ('d', _('Draft')), # 草稿 ('p', _('Published')), # 已发布 ) - # 评论状态选择:开放或关闭 + #cll 评论状态选择:开放或关闭 COMMENT_STATUS = ( ('o', _('Open')), # 开放评论 ('c', _('Close')), # 关闭评论 ) - # 内容类型选择:文章或页面 + #cll 内容类型选择:文章或页面 TYPE = ( ('a', _('Article')), # 普通文章 ('p', _('Page')), # 静态页面 ) - title = models.CharField(_('title'), max_length=200, unique=True) # 文章标题 - body = MDTextField(_('body')) # 文章正文,支持Markdown格式 + title = models.CharField(_('title'), max_length=200, unique=True) #cll 文章标题 + body = MDTextField(_('body')) #cll 文章正文,支持Markdown格式 pub_time = models.DateTimeField( - _('publish time'), blank=False, null=False, default=now) # 发布时间 + _('publish time'), blank=False, null=False, default=now) #cll 发布时间 status = models.CharField( _('status'), max_length=1, choices=STATUS_CHOICES, - default='p') # 文章状态 + default='p') #cll 文章状态 comment_status = models.CharField( _('comment status'), max_length=1, choices=COMMENT_STATUS, - default='o') # 评论状态 - type = models.CharField(_('type'), max_length=1, choices=TYPE, default='a') # 内容类型 - views = models.PositiveIntegerField(_('views'), default=0) # 浏览次数 + default='o') #cll 评论状态 + type = models.CharField(_('type'), max_length=1, choices=TYPE, default='a') #cll 内容类型 + views = models.PositiveIntegerField(_('views'), default=0) #cll 浏览次数 author = models.ForeignKey( settings.AUTH_USER_MODEL, verbose_name=_('author'), blank=False, null=False, - on_delete=models.CASCADE) # 作者,关联用户表 + on_delete=models.CASCADE) #cll 作者,关联用户表 article_order = models.IntegerField( - _('order'), blank=False, null=False, default=0) # 文章排序 - show_toc = models.BooleanField(_('show toc'), blank=False, null=False, default=False) # 是否显示目录 + _('order'), blank=False, null=False, default=0) #cll 文章排序 + show_toc = models.BooleanField(_('show toc'), blank=False, null=False, default=False) #cll 是否显示目录 category = models.ForeignKey( 'Category', verbose_name=_('category'), on_delete=models.CASCADE, blank=False, - null=False) # 分类,关联分类表 - tags = models.ManyToManyField('Tag', verbose_name=_('tag'), blank=True) # 标签,多对多关系 + null=False) #cll 分类,关联分类表 + tags = models.ManyToManyField('Tag', verbose_name=_('tag'), blank=True) #cll 标签,多对多关系 + #cll 将文章内容转换为字符串 def body_to_string(self): - # 将文章正文转换为字符串 return self.body + #cll 返回文章标题作为对象的字符串表示 def __str__(self): - # 返回文章标题作为字符串表示 return self.title class Meta: - ordering = ['-article_order', '-pub_time'] # 按排序字段和发布时间倒序排列 - verbose_name = _('article') # 在管理后台显示的名称 - verbose_name_plural = verbose_name # 复数形式 - get_latest_by = 'id' # 获取最新记录的依据 + ordering = ['-article_order', '-pub_time'] #cll 按排序字段和发布时间倒序排列 + verbose_name = _('article') #cll 在管理后台显示的名称 + verbose_name_plural = verbose_name #cll 复数形式 + get_latest_by = 'id' #cll 获取最新记录的依据 + #cll 获取文章的URL def get_absolute_url(self): - # 获取文章的URL地址,包含年月日信息 - return reverse('blog:detailbyid', kwargs={ - 'article_id': self.id, - 'year': self.creation_time.year, - 'month': self.creation_time.month, - 'day': self.creation_time.day - }) + if self.type == 'a': + return reverse('blog:detail', kwargs={'article_id': self.id, 'slug': self.slug}) + elif self.type == 'p': + return reverse('blog:page', kwargs={'article_id': self.id, 'slug': self.slug}) + #cll 获取分类树,缓存10小时 @cache_decorator(60 * 60 * 10) # 缓存10小时 def get_category_tree(self): - # 获取文章所属分类的层级结构 - tree = self.category.get_category_tree() - names = list(map(lambda c: (c.name, c.get_absolute_url()), tree)) + category = self.category + names = [category.name] + while category.parent_category: + category = category.parent_category + names.append(category.name) return names + #cll 保存文章,更新修改时间 def save(self, *args, **kwargs): - # 保存文章 - super().save(*args, **kwargs) + self.last_modify_time = now() + return super().save(*args, **kwargs) + #cll 增加文章浏览次数 def viewed(self): - # 增加文章浏览次数 self.views += 1 self.save(update_fields=['views']) + #cll 获取文章评论列表 def comment_list(self): - # 获取文章评论列表,使用缓存提高性能 - cache_key = 'article_comments_{id}'.format(id=self.id) - value = cache.get(cache_key) - if value: - logger.info('get article comments:{id}'.format(id=self.id)) - return value - else: - comments = self.comment_set.filter(is_enable=True).order_by('-id') - cache.set(cache_key, comments, 60 * 100) - logger.info('set article comments:{id}'.format(id=self.id)) - return comments + comments = self.comment_set.filter(is_enable=True).order_by('-id') + return comments + #cll 获取文章在管理后台的URL def get_admin_url(self): - # 获取文章在管理后台的编辑URL info = (self._meta.app_label, self._meta.model_name) - return reverse('admin:%s_%s_change' % info, args=(self.pk,)) + return reverse('admin:%s_%s_change' % info, args=(self.id,)) + #cll 获取下一篇文章,缓存100分钟 @cache_decorator(expiration=60 * 100) # 缓存100分钟 def next_article(self): - # 获取下一篇已发布的文章 - return Article.objects.filter( - id__gt=self.id, status='p').order_by('id').first() + return Article.objects.filter(id__gt=self.id, status='p').order_by('id').first() + #cll 获取上一篇文章,缓存100分钟 @cache_decorator(expiration=60 * 100) # 缓存100分钟 def prev_article(self): - # 获取上一篇已发布的文章 - return Article.objects.filter(id__lt=self.id, status='p').first() + return Article.objects.filter(id__lt=self.id, status='p').order_by('-id').first() + #cll 获取文章中的第一张图片URL def get_first_image_url(self): - # 从文章正文中提取第一张图片的URL - match = re.search(r'!\[.*?\]\((.+?)\)', self.body) - if match: - return match.group(1) - return "" + pattern = re.compile(r' 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 - - # 插件钩子:通知插件"文章详情已获取" - hooks.run_action('after_article_body_get', article=article, request=self.request) - # 插件钩子:允许插件修改文章正文 - article.body = hooks.apply_filters(ARTICLE_CONTENT_HOOK_NAME, article.body, article=article, - request=self.request) - - return context + #flj 调用父类方法获取基础上下文数据 + #flj 添加评论表单 + #flj 获取文章评论列表 + #flj 添加相关文章 + #flj 调用插件处理文章内容 + return super().get_context_data(**kwargs) +#flj 分类详情视图,显示指定分类下的文章列表 class CategoryDetailView(ArticleListView): ''' - 分类目录列表视图,显示指定分类下的所有文章 + 分类详情页视图 ''' - page_type = "分类目录归档" + page_type = "分类目录归档" #flj 页面类型标识 + #flj 获取分类下的文章数据,根据URL参数中的分类ID过滤 def get_queryset_data(self): - # 获取指定分类下的所有文章(包括子分类) - 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') + #flj 获取分类ID + #flj 过滤该分类下的已发布文章 + #flj 返回查询结果 return article_list + #flj 生成分类页面的缓存键,包含分类ID和页码 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) + #flj 生成缓存键 return cache_key + #flj 添加分类信息到上下文 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) + #flj 获取分类对象 + #flj 添加到上下文 + return super().get_context_data(**kwargs) +#flj 作者详情视图,显示指定作者的文章列表 class AuthorDetailView(ArticleListView): ''' - 作者详情页视图,显示指定作者的所有文章 + 作者详情页视图 ''' - page_type = '作者文章归档' + page_type = '作者文章归档' #flj 页面类型标识 + #flj 生成作者页面的缓存键,包含作者ID和页码 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) + #flj 生成缓存键 return cache_key + #flj 获取作者的文章数据,根据URL参数中的作者ID过滤 def get_queryset_data(self): - # 获取指定作者的所有已发布文章 - author_name = self.kwargs['author_name'] - article_list = Article.objects.filter( - author__username=author_name, type='a', status='p') + #flj 获取作者ID + #flj 过滤该作者的已发布文章 return article_list + #flj 添加作者信息到上下文 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) + #flj 获取作者对象 + #flj 添加到上下文 + return super().get_context_data(**kwargs) +#flj 标签详情视图,显示指定标签下的文章列表 class TagDetailView(ArticleListView): ''' - 标签列表页面视图,显示指定标签下的所有文章 + 标签详情页视图 ''' - page_type = '分类标签归档' + page_type = '分类标签归档' #flj 页面类型标识 + #flj 获取标签下的文章数据,根据URL参数中的标签ID过滤 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') + #flj 获取标签ID + #flj 过滤该标签下的已发布文章 return article_list + #flj 生成标签页面的缓存键,包含标签ID和页码 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) + #flj 生成缓存键 return cache_key + #flj 添加标签信息到上下文 def get_context_data(self, **kwargs): - # 为标签页面添加上下文数据 - tag_name = self.name - kwargs['page_type'] = TagDetailView.page_type - kwargs['tag_name'] = tag_name - return super(TagDetailView, self).get_context_data(**kwargs) + #flj 获取标签对象 + #flj 添加到上下文 + return super().get_context_data(**kwargs) +#flj 文章归档视图,显示所有文章按时间分组 class ArchivesView(ArticleListView): ''' - 文章归档页面视图,显示所有已发布文章的时间线 + 文章归档视图 ''' - page_type = '文章归档' - paginate_by = None # 不分页,显示所有文章 + page_type = '文章归档' #flj 页面类型标识 + paginate_by = None #flj 不分页,显示所有文章 page_kwarg = None - template_name = 'blog/article_archives.html' + template_name = 'blog/article_archives.html' #flj 使用归档专用模板 + #flj 获取所有已发布文章,按时间排序 def get_queryset_data(self): - # 获取所有已发布的文章 - return Article.objects.filter(status='p').all() + #flj 获取所有已发布文章 + return article_list + #flj 生成归档页面的缓存键 def get_queryset_cache_key(self): - # 归档页面的缓存键 - cache_key = 'archives' + #flj 生成缓存键 return cache_key +#flj 友情链接列表视图 class LinkListView(ListView): - """友情链接列表视图""" - model = Links - template_name = 'blog/links_list.html' + ''' + 友情链接列表视图 + ''' + model = Links #flj 关联的模型 + template_name = 'blog/links_list.html' #flj 使用的模板 + #flj 获取友情链接数据,按显示顺序排序 def get_queryset(self): - # 获取所有启用的友情链接 - return Links.objects.filter(is_enable=True) + #flj 过滤显示状态的友情链接并排序 + return links +#flj 搜索视图,处理文章搜索功能 class EsSearchView(SearchView): - """Elasticsearch搜索视图""" + ''' + 搜索视图 + ''' + #flj 获取搜索结果的上下文数据 def get_context(self): - # 为搜索页面准备上下文数据 - 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()) - + #flj 获取基础上下文 + #flj 添加额外的搜索相关信息 return context -@csrf_exempt # 跳过CSRF验证,因为这是文件上传接口 +#flj 文件上传接口,允许上传图片等文件 +@csrf_exempt #flj 跳过CSRF验证,因为这是文件上传接口 def fileupload(request): - """ - 文件上传接口,提供图床功能 - 需要提供正确的签名才能上传文件 - :param request: HTTP请求对象 - :return: 上传成功返回文件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 - url = static(savepath) - response.append(url) - return HttpResponse(response) - - else: - return HttpResponse("only for post") + ''' + 文件上传接口 + ''' + #flj 检查请求方法 + #flj 验证权限 + #flj 处理文件上传 + #flj 保存文件到指定目录 + #flj 返回文件URL + return HttpResponse(json.dumps(data), content_type="application/json") +#flj 404错误页面视图 +#flj 处理页面未找到的情况 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) + ''' + 404错误页面 + ''' + #flj 渲染错误页面 + return render(request, template_name, context, status=404) +#flj 500错误页面视图 +#flj 处理服务器内部错误的情况 def server_error_view(request, template_name='blog/error_page.html'): - """500服务器错误页面处理函数""" - return render(request, - template_name, - {'message': _('Sorry, the server is busy, please click the home page to see other?'), - 'statuscode': '500'}, - status=500) + ''' + 500错误页面 + ''' + #flj 渲染错误页面 + return render(request, template_name, context, status=500) +#flj 403错误页面视图 +#flj 处理权限拒绝的情况 def permission_denied_view( request, exception, template_name='blog/error_page.html'): - """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) + ''' + 403错误页面 + ''' + #flj 渲染错误页面 + return render(request, template_name, context, status=403) +#flj 清理缓存视图,用于手动清理站点缓存 +#flj 提供管理功能,清除系统缓存 def clean_cache_view(request): - """清除缓存的管理接口""" - cache.clear() - return HttpResponse('ok') + ''' + 清理缓存视图 + ''' + #flj 验证用户权限 + #flj 清理缓存 + #flj 返回成功信息 + return HttpResponse(_('清理缓存成功'))