lxy_branch
刘夏渝 5 months ago
parent 1abaf8fdfb
commit 24ee8aefe6

@ -0,0 +1,149 @@
# lxy: !/usr/bin/env python3
# lxy: -*- coding: utf-8 -*-
"""
给Python文件注释添加"lxy:"前缀的脚本
"""
import os
import re
import argparse
from pathlib import Path
def add_lxy_to_comments(file_path, dry_run=False):
"""
给单个文件的注释添加"lxy:"前缀
Args:
file_path: 文件路径
dry_run: 是否为试运行模式不实际修改文件
Returns:
tuple: (修改的行数, 文件总行数)
"""
try:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
except UnicodeDecodeError:
try:
with open(file_path, 'r', encoding='gbk') as f:
content = f.read()
except Exception as e:
print(f"无法读取文件 {file_path}: {e}")
return 0, 0
lines = content.split('\n')
modified_lines = []
modified_count = 0
# lxy: 匹配单行注释的正则表达式
single_line_comment_pattern = re.compile(r'^(\s*)#\s*(?!lxy:)(.*)$')
for line in lines:
# lxy: 检查是否是单行注释
match = single_line_comment_pattern.match(line)
if match:
# lxy: 保留原有的缩进,在注释内容前添加"lxy: "
indent = match.group(1)
comment_content = match.group(2).strip()
# lxy: 如果注释内容不为空且不是以lxy:开头,则添加前缀
if comment_content and not comment_content.startswith('lxy:'):
new_line = f"{indent}# lxy: {comment_content}"
modified_lines.append(new_line)
modified_count += 1
else:
modified_lines.append(line)
else:
modified_lines.append(line)
if modified_count > 0 and not dry_run:
try:
with open(file_path, 'w', encoding='utf-8') as f:
f.write('\n'.join(modified_lines))
except Exception as e:
print(f"写入文件 {file_path} 时出错: {e}")
return 0, len(lines)
return modified_count, len(lines)
def process_directory(directory='.', dry_run=False, recursive=False):
"""
处理目录下的所有Python文件
Args:
directory: 要处理的目录
dry_run: 是否为试运行模式
recursive: 是否递归处理子目录
"""
py_files = []
dir_path = Path(directory)
if recursive:
pattern = '**/*.py'
else:
pattern = '*.py'
for py_file in dir_path.glob(pattern):
# lxy: 跳过隐藏文件和目录
if any(part.startswith('.') for part in py_file.parts):
continue
py_files.append(py_file)
if not py_files:
print(f"在目录 {directory} 中未找到Python文件")
return
print(f"找到 {len(py_files)} 个Python文件")
total_modified = 0
total_files_processed = 0
for py_file in py_files:
print(f"\n处理文件: {py_file}")
modified_count, total_lines = add_lxy_to_comments(py_file, dry_run)
if modified_count > 0:
if dry_run:
print(f" [试运行] 将修改 {modified_count}/{total_lines} 行注释")
else:
print(f" 已修改 {modified_count}/{total_lines} 行注释")
total_modified += modified_count
else:
print(f" 无需修改")
total_files_processed += 1
print(f"\n{'='*50}")
if dry_run:
print(f"[试运行完成] 共检查 {total_files_processed} 个文件")
print(f"预计将修改 {total_modified} 行注释")
else:
print(f"处理完成!共处理 {total_files_processed} 个文件")
print(f"实际修改 {total_modified} 行注释")
def main():
parser = argparse.ArgumentParser(description='给Python文件注释添加"lxy:"前缀')
parser.add_argument('--dry-run', action='store_true',
help='试运行模式,显示将要修改的内容但不实际修改文件')
parser.add_argument('--recursive', '-r', action='store_true',
help='递归处理子目录')
parser.add_argument('--directory', '-d', default='.',
help='要处理的目录路径(默认为当前目录)')
args = parser.parse_args()
print("Python文件注释处理工具")
print("功能: 给所有注释添加'lxy:'前缀")
print("=" * 50)
if args.dry_run:
print("模式: 试运行(不会实际修改文件)")
try:
process_directory(args.directory, args.dry_run, args.recursive)
except KeyboardInterrupt:
print("\n用户中断操作")
except Exception as e:
print(f"处理过程中出错: {e}")
if __name__ == "__main__":
main()

@ -1,104 +1,104 @@
# 导入Django表单模块用于创建自定义表单
# lxy: 导入Django表单模块用于创建自定义表单
from django import forms
# 导入Django admin模块用于配置后台管理界面
# lxy: 导入Django admin模块用于配置后台管理界面
from django.contrib import admin
# 导入Django用户模型获取工具兼容自定义用户模型场景
# lxy: 导入Django用户模型获取工具兼容自定义用户模型场景
from django.contrib.auth import get_user_model
# 导入Django URL反向解析模块用于生成后台管理页面的链接
# lxy: 导入Django URL反向解析模块用于生成后台管理页面的链接
from django.urls import reverse
# 导入Django HTML格式化工具用于在后台显示自定义HTML内容如链接
# lxy: 导入Django HTML格式化工具用于在后台显示自定义HTML内容如链接
from django.utils.html import format_html
# 导入Django国际化翻译工具用于实现后台文字的多语言支持
# lxy: 导入Django国际化翻译工具用于实现后台文字的多语言支持
from django.utils.translation import gettext_lazy as _
# 注册自定义模型到admin后台的标识注释固定写法
# Register your models here.
# 从当前应用的models.py文件中导入Article模型文章模型
# lxy: 注册自定义模型到admin后台的标识注释固定写法
# lxy: Register your models here.
# lxy: 从当前应用的models.py文件中导入Article模型文章模型
from .models import Article
# 自定义Article模型的列表页过滤器继承自Django内置的SimpleListFilter
# lxy: 自定义Article模型的列表页过滤器继承自Django内置的SimpleListFilter
class ArticleListFilter(admin.SimpleListFilter):
# 过滤器在后台页面显示的标题(支持国际化),这里是“作者”
# lxy: 过滤器在后台页面显示的标题(支持国际化),这里是“作者”
title = _("author")
# 过滤器对应的URL参数名用于URL中传递过滤条件如?author=1
# lxy: 过滤器对应的URL参数名用于URL中传递过滤条件如?author=1
parameter_name = 'author'
# 定义过滤器的选项列表,返回(值, 显示文本)格式的元组
# lxy: 定义过滤器的选项列表,返回(值, 显示文本)格式的元组
def lookups(self, request, model_admin):
# 1. 从Article模型中获取所有文章的作者用set去重避免同一作者多次出现
# 2. 转换为list便于遍历map函数提取每篇文章的author字段
# lxy: 1. 从Article模型中获取所有文章的作者用set去重避免同一作者多次出现
# lxy: 2. 转换为list便于遍历map函数提取每篇文章的author字段
authors = list(set(map(lambda x: x.author, Article.objects.all())))
# 遍历去重后的作者列表,生成过滤器选项
# lxy: 遍历去重后的作者列表,生成过滤器选项
for author in authors:
# 选项值为作者ID用于数据库查询显示文本为作者用户名支持国际化
# lxy: 选项值为作者ID用于数据库查询显示文本为作者用户名支持国际化
yield (author.id, _(author.username))
# 根据过滤器选择的参数过滤文章查询集queryset
# lxy: 根据过滤器选择的参数过滤文章查询集queryset
def queryset(self, request, queryset):
# 获取当前过滤器选中的参数值即作者ID
# lxy: 获取当前过滤器选中的参数值即作者ID
id = self.value()
# 如果有选中的作者ID返回该作者的所有文章
# lxy: 如果有选中的作者ID返回该作者的所有文章
if id:
return queryset.filter(author__id__exact=id)
# 如果未选中任何作者,返回全部文章查询集
# lxy: 如果未选中任何作者,返回全部文章查询集
else:
return queryset
# 自定义Article模型的表单类继承自Django内置的ModelForm
# lxy: 自定义Article模型的表单类继承自Django内置的ModelForm
class ArticleForm(forms.ModelForm):
# 注释此处原本计划使用AdminPagedownWidget富文本编辑器渲染body字段暂未启用
# body = forms.CharField(widget=AdminPagedownWidget())
# lxy: 注释此处原本计划使用AdminPagedownWidget富文本编辑器渲染body字段暂未启用
# lxy: body = forms.CharField(widget=AdminPagedownWidget())
# 表单元数据配置类,用于关联模型与字段
# lxy: 表单元数据配置类,用于关联模型与字段
class Meta:
# 关联的模型为Article表示该表单用于操作Article模型数据
# lxy: 关联的模型为Article表示该表单用于操作Article模型数据
model = Article
# 表单包含模型的所有字段__all__为通配符也可指定具体字段列表
# lxy: 表单包含模型的所有字段__all__为通配符也可指定具体字段列表
fields = '__all__'
# 自定义批量操作函数将选中的文章状态设为“已发布”假设status='p'代表发布)
# lxy: 自定义批量操作函数将选中的文章状态设为“已发布”假设status='p'代表发布)
def makr_article_publish(modeladmin, request, queryset):
# 批量更新选中的文章查询集将status字段设为'p'
# lxy: 批量更新选中的文章查询集将status字段设为'p'
queryset.update(status='p')
# 自定义批量操作函数将选中的文章状态设为“草稿”假设status='d'代表草稿)
# lxy: 自定义批量操作函数将选中的文章状态设为“草稿”假设status='d'代表草稿)
def draft_article(modeladmin, request, queryset):
# 批量更新选中的文章查询集将status字段设为'd'
# lxy: 批量更新选中的文章查询集将status字段设为'd'
queryset.update(status='d')
# 自定义批量操作函数关闭选中文章的评论功能假设comment_status='c'代表关闭)
# lxy: 自定义批量操作函数关闭选中文章的评论功能假设comment_status='c'代表关闭)
def close_article_commentstatus(modeladmin, request, queryset):
# 批量更新选中的文章查询集将comment_status字段设为'c'
# lxy: 批量更新选中的文章查询集将comment_status字段设为'c'
queryset.update(comment_status='c')
# 自定义批量操作函数开启选中文章的评论功能假设comment_status='o'代表开启)
# lxy: 自定义批量操作函数开启选中文章的评论功能假设comment_status='o'代表开启)
def open_article_commentstatus(modeladmin, request, queryset):
# 批量更新选中的文章查询集将comment_status字段设为'o'
# lxy: 批量更新选中的文章查询集将comment_status字段设为'o'
queryset.update(comment_status='o')
# 为批量操作函数设置后台显示名称(支持国际化),显示在“动作”下拉菜单中
# lxy: 为批量操作函数设置后台显示名称(支持国际化),显示在“动作”下拉菜单中
makr_article_publish.short_description = _('Publish selected articles') # “发布选中的文章”
draft_article.short_description = _('Draft selected articles') # “将选中的文章设为草稿”
close_article_commentstatus.short_description = _('Close article comments') # “关闭选中文章的评论”
open_article_commentstatus.short_description = _('Open article comments') # “开启选中文章的评论”
# 自定义Article模型的Admin配置类继承自Django内置的ModelAdmin
# lxy: 自定义Article模型的Admin配置类继承自Django内置的ModelAdmin
class ArticlelAdmin(admin.ModelAdmin):
# 后台列表页每页显示的文章数量这里是20条/页
# lxy: 后台列表页每页显示的文章数量这里是20条/页
list_per_page = 20
# 后台列表页的搜索框配置支持搜索文章的“内容body”和“标题title”字段
# lxy: 后台列表页的搜索框配置支持搜索文章的“内容body”和“标题title”字段
search_fields = ('body', 'title')
# 后台添加/编辑文章时使用的自定义表单即上面定义的ArticleForm
# lxy: 后台添加/编辑文章时使用的自定义表单即上面定义的ArticleForm
form = ArticleForm
# 后台列表页显示的字段列表,按顺序展示
# lxy: 后台列表页显示的字段列表,按顺序展示
list_display = (
'id', # 文章ID
'title', # 文章标题
@ -110,95 +110,95 @@ class ArticlelAdmin(admin.ModelAdmin):
'type', # 文章类型(如原创/转载等需在Article模型中定义
'article_order' # 文章排序权重(用于自定义排序)
)
# 后台列表页中,点击哪些字段可以跳转到文章编辑页
# lxy: 后台列表页中,点击哪些字段可以跳转到文章编辑页
list_display_links = ('id', 'title')
# 后台列表页的过滤器配置,包含自定义的作者过滤器和内置字段过滤器
# lxy: 后台列表页的过滤器配置,包含自定义的作者过滤器和内置字段过滤器
list_filter = (ArticleListFilter, 'status', 'type', 'category', 'tags')
# 多对多字段tags标签的编辑界面样式使用水平选择框默认是垂直
# lxy: 多对多字段tags标签的编辑界面样式使用水平选择框默认是垂直
filter_horizontal = ('tags',)
# 后台添加/编辑文章时,隐藏的字段(不允许管理员手动修改)
# lxy: 后台添加/编辑文章时,隐藏的字段(不允许管理员手动修改)
exclude = ('creation_time', 'last_modify_time')
# 是否显示“在站点上查看”按钮(跳转到文章的前台页面)
# lxy: 是否显示“在站点上查看”按钮(跳转到文章的前台页面)
view_on_site = True
# 后台列表页的批量操作功能列表关联上面定义的4个批量操作函数
# lxy: 后台列表页的批量操作功能列表关联上面定义的4个批量操作函数
actions = [
makr_article_publish,
draft_article,
close_article_commentstatus,
open_article_commentstatus]
# 自定义列表页字段:显示文章分类,并添加跳转到分类编辑页的链接
# lxy: 自定义列表页字段:显示文章分类,并添加跳转到分类编辑页的链接
def link_to_category(self, obj):
# 1. 获取文章分类obj.category的模型元数据应用名、模型名
# 2. 用于生成admin后台分类编辑页的URL
# lxy: 1. 获取文章分类obj.category的模型元数据应用名、模型名
# lxy: 2. 用于生成admin后台分类编辑页的URL
info = (obj.category._meta.app_label, obj.category._meta.model_name)
# 3. 反向解析分类编辑页的URL参数为分类IDobj.category.id
# lxy: 3. 反向解析分类编辑页的URL参数为分类IDobj.category.id
link = reverse('admin:%s_%s_change' % info, args=(obj.category.id,))
# 4. 生成HTML链接显示分类名称点击跳转到分类编辑页
# lxy: 4. 生成HTML链接显示分类名称点击跳转到分类编辑页
return format_html(u'<a href="%s">%s</a>' % (link, obj.category.name))
# 自定义字段“link_to_category”在后台列表页的显示标题支持国际化
# lxy: 自定义字段“link_to_category”在后台列表页的显示标题支持国际化
link_to_category.short_description = _('category')
# 重写获取表单的方法自定义作者字段author的可选值
# lxy: 重写获取表单的方法自定义作者字段author的可选值
def get_form(self, request, obj=None, **kwargs):
# 1. 先调用父类的get_form方法获取默认表单
# lxy: 1. 先调用父类的get_form方法获取默认表单
form = super(ArticlelAdmin, self).get_form(request, obj, **kwargs)
# 2. 限制作者字段的可选范围仅显示超级用户is_superuser=True
# lxy: 2. 限制作者字段的可选范围仅显示超级用户is_superuser=True
form.base_fields['author'].queryset = get_user_model(
).objects.filter(is_superuser=True)
# 3. 返回修改后的表单
# lxy: 3. 返回修改后的表单
return form
# 重写保存模型的方法(此处未修改逻辑,仅保留父类功能,便于后续扩展)
# lxy: 重写保存模型的方法(此处未修改逻辑,仅保留父类功能,便于后续扩展)
def save_model(self, request, obj, form, change):
# 调用父类的save_model方法完成默认的保存逻辑如更新时间、权限验证等
# lxy: 调用父类的save_model方法完成默认的保存逻辑如更新时间、权限验证等
super(ArticlelAdmin, self).save_model(request, obj, form, change)
# 重写“在站点上查看”的链接地址跳转到文章的前台完整URL
# lxy: 重写“在站点上查看”的链接地址跳转到文章的前台完整URL
def get_view_on_site_url(self, obj=None):
# 如果有具体的文章对象obj不为空调用文章模型的get_full_url方法获取前台URL
# lxy: 如果有具体的文章对象obj不为空调用文章模型的get_full_url方法获取前台URL
if obj:
url = obj.get_full_url()
return url
# 如果没有文章对象(如在列表页顶部的“查看站点”按钮),返回当前站点的域名
# lxy: 如果没有文章对象(如在列表页顶部的“查看站点”按钮),返回当前站点的域名
else:
# 从自定义工具模块导入获取当前站点的函数需确保djangoblog.utils存在
# lxy: 从自定义工具模块导入获取当前站点的函数需确保djangoblog.utils存在
from djangoblog.utils import get_current_site
# 获取当前站点的域名如www.example.com
# lxy: 获取当前站点的域名如www.example.com
site = get_current_site().domain
return site
# 自定义Tag模型标签模型的Admin配置类假设Tag模型在models.py中定义
# lxy: 自定义Tag模型标签模型的Admin配置类假设Tag模型在models.py中定义
class TagAdmin(admin.ModelAdmin):
# 后台添加/编辑标签时,隐藏的字段(不允许手动修改)
# lxy: 后台添加/编辑标签时,隐藏的字段(不允许手动修改)
exclude = ('slug', 'last_mod_time', 'creation_time')
# 自定义Category模型分类模型的Admin配置类假设Category模型在models.py中定义
# lxy: 自定义Category模型分类模型的Admin配置类假设Category模型在models.py中定义
class CategoryAdmin(admin.ModelAdmin):
# 后台分类列表页显示的字段:分类名称、父分类、排序索引
# lxy: 后台分类列表页显示的字段:分类名称、父分类、排序索引
list_display = ('name', 'parent_category', 'index')
# 后台添加/编辑分类时,隐藏的字段(不允许手动修改)
# lxy: 后台添加/编辑分类时,隐藏的字段(不允许手动修改)
exclude = ('slug', 'last_mod_time', 'creation_time')
# 自定义Links模型友情链接模型的Admin配置类假设Links模型在models.py中定义
# lxy: 自定义Links模型友情链接模型的Admin配置类假设Links模型在models.py中定义
class LinksAdmin(admin.ModelAdmin):
# 后台添加/编辑友情链接时,隐藏的字段(不允许手动修改)
# lxy: 后台添加/编辑友情链接时,隐藏的字段(不允许手动修改)
exclude = ('last_mod_time', 'creation_time')
# 自定义SideBar模型侧边栏模型的Admin配置类假设SideBar模型在models.py中定义
# lxy: 自定义SideBar模型侧边栏模型的Admin配置类假设SideBar模型在models.py中定义
class SideBarAdmin(admin.ModelAdmin):
# 后台侧边栏列表页显示的字段:名称、内容、是否启用、排序序号
# lxy: 后台侧边栏列表页显示的字段:名称、内容、是否启用、排序序号
list_display = ('name', 'content', 'is_enable', 'sequence')
# 后台添加/编辑侧边栏时,隐藏的字段(不允许手动修改)
# lxy: 后台添加/编辑侧边栏时,隐藏的字段(不允许手动修改)
exclude = ('last_mod_time', 'creation_time')
# 自定义BlogSettings模型博客设置模型的Admin配置类假设BlogSettings模型在models.py中定义
# lxy: 自定义BlogSettings模型博客设置模型的Admin配置类假设BlogSettings模型在models.py中定义
class BlogSettingsAdmin(admin.ModelAdmin):
# 空配置使用ModelAdmin的默认功能如需自定义可后续添加字段
# lxy: 空配置使用ModelAdmin的默认功能如需自定义可后续添加字段
pass

@ -1,9 +1,9 @@
# 导入Django的AppConfig类该类用于定义单个Django应用的配置信息
# lxy: 导入Django的AppConfig类该类用于定义单个Django应用的配置信息
from django.apps import AppConfig
# 定义当前应用blog的配置类继承自Django提供的AppConfig基类
# lxy: 定义当前应用blog的配置类继承自Django提供的AppConfig基类
class BlogConfig(AppConfig):
# 配置当前应用的唯一标识名称即应用目录名Django通过该名称识别和管理应用
# 这里'blog'表示当前配置对应的是名为'blog'的Django应用
# lxy: 配置当前应用的唯一标识名称即应用目录名Django通过该名称识别和管理应用
# lxy: 这里'blog'表示当前配置对应的是名为'blog'的Django应用
name = 'blog'

@ -1,104 +1,104 @@
# 导入Django表单模块用于创建自定义表单
# lxy: 导入Django表单模块用于创建自定义表单
from django import forms
# 导入Django admin模块用于配置后台管理界面
# lxy: 导入Django admin模块用于配置后台管理界面
from django.contrib import admin
# 导入Django用户模型获取工具兼容自定义用户模型场景
# lxy: 导入Django用户模型获取工具兼容自定义用户模型场景
from django.contrib.auth import get_user_model
# 导入Django URL反向解析模块用于生成后台管理页面的链接
# lxy: 导入Django URL反向解析模块用于生成后台管理页面的链接
from django.urls import reverse
# 导入Django HTML格式化工具用于在后台显示自定义HTML内容如链接
# lxy: 导入Django HTML格式化工具用于在后台显示自定义HTML内容如链接
from django.utils.html import format_html
# 导入Django国际化翻译工具用于实现后台文字的多语言支持
# lxy: 导入Django国际化翻译工具用于实现后台文字的多语言支持
from django.utils.translation import gettext_lazy as _
# 注册自定义模型到admin后台的标识注释固定写法
# Register your models here.
# 从当前应用的models.py文件中导入Article模型文章模型
# lxy: 注册自定义模型到admin后台的标识注释固定写法
# lxy: Register your models here.
# lxy: 从当前应用的models.py文件中导入Article模型文章模型
from .models import Article
# 自定义Article模型的列表页过滤器继承自Django内置的SimpleListFilter
# lxy: 自定义Article模型的列表页过滤器继承自Django内置的SimpleListFilter
class ArticleListFilter(admin.SimpleListFilter):
# 过滤器在后台页面显示的标题(支持国际化),这里是“作者”
# lxy: 过滤器在后台页面显示的标题(支持国际化),这里是“作者”
title = _("author")
# 过滤器对应的URL参数名用于URL中传递过滤条件如?author=1
# lxy: 过滤器对应的URL参数名用于URL中传递过滤条件如?author=1
parameter_name = 'author'
# 定义过滤器的选项列表,返回(值, 显示文本)格式的元组
# lxy: 定义过滤器的选项列表,返回(值, 显示文本)格式的元组
def lookups(self, request, model_admin):
# 1. 从Article模型中获取所有文章的作者用set去重避免同一作者多次出现
# 2. 转换为list便于遍历map函数提取每篇文章的author字段
# lxy: 1. 从Article模型中获取所有文章的作者用set去重避免同一作者多次出现
# lxy: 2. 转换为list便于遍历map函数提取每篇文章的author字段
authors = list(set(map(lambda x: x.author, Article.objects.all())))
# 遍历去重后的作者列表,生成过滤器选项
# lxy: 遍历去重后的作者列表,生成过滤器选项
for author in authors:
# 选项值为作者ID用于数据库查询显示文本为作者用户名支持国际化
# lxy: 选项值为作者ID用于数据库查询显示文本为作者用户名支持国际化
yield (author.id, _(author.username))
# 根据过滤器选择的参数过滤文章查询集queryset
# lxy: 根据过滤器选择的参数过滤文章查询集queryset
def queryset(self, request, queryset):
# 获取当前过滤器选中的参数值即作者ID
# lxy: 获取当前过滤器选中的参数值即作者ID
id = self.value()
# 如果有选中的作者ID返回该作者的所有文章
# lxy: 如果有选中的作者ID返回该作者的所有文章
if id:
return queryset.filter(author__id__exact=id)
# 如果未选中任何作者,返回全部文章查询集
# lxy: 如果未选中任何作者,返回全部文章查询集
else:
return queryset
# 自定义Article模型的表单类继承自Django内置的ModelForm
# lxy: 自定义Article模型的表单类继承自Django内置的ModelForm
class ArticleForm(forms.ModelForm):
# 注释此处原本计划使用AdminPagedownWidget富文本编辑器渲染body字段暂未启用
# body = forms.CharField(widget=AdminPagedownWidget())
# lxy: 注释此处原本计划使用AdminPagedownWidget富文本编辑器渲染body字段暂未启用
# lxy: body = forms.CharField(widget=AdminPagedownWidget())
# 表单元数据配置类,用于关联模型与字段
# lxy: 表单元数据配置类,用于关联模型与字段
class Meta:
# 关联的模型为Article表示该表单用于操作Article模型数据
# lxy: 关联的模型为Article表示该表单用于操作Article模型数据
model = Article
# 表单包含模型的所有字段__all__为通配符也可指定具体字段列表
# lxy: 表单包含模型的所有字段__all__为通配符也可指定具体字段列表
fields = '__all__'
# 自定义批量操作函数将选中的文章状态设为“已发布”假设status='p'代表发布)
# lxy: 自定义批量操作函数将选中的文章状态设为“已发布”假设status='p'代表发布)
def makr_article_publish(modeladmin, request, queryset):
# 批量更新选中的文章查询集将status字段设为'p'
# lxy: 批量更新选中的文章查询集将status字段设为'p'
queryset.update(status='p')
# 自定义批量操作函数将选中的文章状态设为“草稿”假设status='d'代表草稿)
# lxy: 自定义批量操作函数将选中的文章状态设为“草稿”假设status='d'代表草稿)
def draft_article(modeladmin, request, queryset):
# 批量更新选中的文章查询集将status字段设为'd'
# lxy: 批量更新选中的文章查询集将status字段设为'd'
queryset.update(status='d')
# 自定义批量操作函数关闭选中文章的评论功能假设comment_status='c'代表关闭)
# lxy: 自定义批量操作函数关闭选中文章的评论功能假设comment_status='c'代表关闭)
def close_article_commentstatus(modeladmin, request, queryset):
# 批量更新选中的文章查询集将comment_status字段设为'c'
# lxy: 批量更新选中的文章查询集将comment_status字段设为'c'
queryset.update(comment_status='c')
# 自定义批量操作函数开启选中文章的评论功能假设comment_status='o'代表开启)
# lxy: 自定义批量操作函数开启选中文章的评论功能假设comment_status='o'代表开启)
def open_article_commentstatus(modeladmin, request, queryset):
# 批量更新选中的文章查询集将comment_status字段设为'o'
# lxy: 批量更新选中的文章查询集将comment_status字段设为'o'
queryset.update(comment_status='o')
# 为批量操作函数设置后台显示名称(支持国际化),显示在“动作”下拉菜单中
# lxy: 为批量操作函数设置后台显示名称(支持国际化),显示在“动作”下拉菜单中
makr_article_publish.short_description = _('Publish selected articles') # “发布选中的文章”
draft_article.short_description = _('Draft selected articles') # “将选中的文章设为草稿”
close_article_commentstatus.short_description = _('Close article comments') # “关闭选中文章的评论”
open_article_commentstatus.short_description = _('Open article comments') # “开启选中文章的评论”
# 自定义Article模型的Admin配置类继承自Django内置的ModelAdmin
# lxy: 自定义Article模型的Admin配置类继承自Django内置的ModelAdmin
class ArticlelAdmin(admin.ModelAdmin):
# 后台列表页每页显示的文章数量这里是20条/页
# lxy: 后台列表页每页显示的文章数量这里是20条/页
list_per_page = 20
# 后台列表页的搜索框配置支持搜索文章的“内容body”和“标题title”字段
# lxy: 后台列表页的搜索框配置支持搜索文章的“内容body”和“标题title”字段
search_fields = ('body', 'title')
# 后台添加/编辑文章时使用的自定义表单即上面定义的ArticleForm
# lxy: 后台添加/编辑文章时使用的自定义表单即上面定义的ArticleForm
form = ArticleForm
# 后台列表页显示的字段列表,按顺序展示
# lxy: 后台列表页显示的字段列表,按顺序展示
list_display = (
'id', # 文章ID
'title', # 文章标题
@ -110,95 +110,95 @@ class ArticlelAdmin(admin.ModelAdmin):
'type', # 文章类型(如原创/转载等需在Article模型中定义
'article_order' # 文章排序权重(用于自定义排序)
)
# 后台列表页中,点击哪些字段可以跳转到文章编辑页
# lxy: 后台列表页中,点击哪些字段可以跳转到文章编辑页
list_display_links = ('id', 'title')
# 后台列表页的过滤器配置,包含自定义的作者过滤器和内置字段过滤器
# lxy: 后台列表页的过滤器配置,包含自定义的作者过滤器和内置字段过滤器
list_filter = (ArticleListFilter, 'status', 'type', 'category', 'tags')
# 多对多字段tags标签的编辑界面样式使用水平选择框默认是垂直
# lxy: 多对多字段tags标签的编辑界面样式使用水平选择框默认是垂直
filter_horizontal = ('tags',)
# 后台添加/编辑文章时,隐藏的字段(不允许管理员手动修改)
# lxy: 后台添加/编辑文章时,隐藏的字段(不允许管理员手动修改)
exclude = ('creation_time', 'last_modify_time')
# 是否显示“在站点上查看”按钮(跳转到文章的前台页面)
# lxy: 是否显示“在站点上查看”按钮(跳转到文章的前台页面)
view_on_site = True
# 后台列表页的批量操作功能列表关联上面定义的4个批量操作函数
# lxy: 后台列表页的批量操作功能列表关联上面定义的4个批量操作函数
actions = [
makr_article_publish,
draft_article,
close_article_commentstatus,
open_article_commentstatus]
# 自定义列表页字段:显示文章分类,并添加跳转到分类编辑页的链接
# lxy: 自定义列表页字段:显示文章分类,并添加跳转到分类编辑页的链接
def link_to_category(self, obj):
# 1. 获取文章分类obj.category的模型元数据应用名、模型名
# 2. 用于生成admin后台分类编辑页的URL
# lxy: 1. 获取文章分类obj.category的模型元数据应用名、模型名
# lxy: 2. 用于生成admin后台分类编辑页的URL
info = (obj.category._meta.app_label, obj.category._meta.model_name)
# 3. 反向解析分类编辑页的URL参数为分类IDobj.category.id
# lxy: 3. 反向解析分类编辑页的URL参数为分类IDobj.category.id
link = reverse('admin:%s_%s_change' % info, args=(obj.category.id,))
# 4. 生成HTML链接显示分类名称点击跳转到分类编辑页
# lxy: 4. 生成HTML链接显示分类名称点击跳转到分类编辑页
return format_html(u'<a href="%s">%s</a>' % (link, obj.category.name))
# 自定义字段“link_to_category”在后台列表页的显示标题支持国际化
# lxy: 自定义字段“link_to_category”在后台列表页的显示标题支持国际化
link_to_category.short_description = _('category')
# 重写获取表单的方法自定义作者字段author的可选值
# lxy: 重写获取表单的方法自定义作者字段author的可选值
def get_form(self, request, obj=None, **kwargs):
# 1. 先调用父类的get_form方法获取默认表单
# lxy: 1. 先调用父类的get_form方法获取默认表单
form = super(ArticlelAdmin, self).get_form(request, obj, **kwargs)
# 2. 限制作者字段的可选范围仅显示超级用户is_superuser=True
# lxy: 2. 限制作者字段的可选范围仅显示超级用户is_superuser=True
form.base_fields['author'].queryset = get_user_model(
).objects.filter(is_superuser=True)
# 3. 返回修改后的表单
# lxy: 3. 返回修改后的表单
return form
# 重写保存模型的方法(此处未修改逻辑,仅保留父类功能,便于后续扩展)
# lxy: 重写保存模型的方法(此处未修改逻辑,仅保留父类功能,便于后续扩展)
def save_model(self, request, obj, form, change):
# 调用父类的save_model方法完成默认的保存逻辑如更新时间、权限验证等
# lxy: 调用父类的save_model方法完成默认的保存逻辑如更新时间、权限验证等
super(ArticlelAdmin, self).save_model(request, obj, form, change)
# 重写“在站点上查看”的链接地址跳转到文章的前台完整URL
# lxy: 重写“在站点上查看”的链接地址跳转到文章的前台完整URL
def get_view_on_site_url(self, obj=None):
# 如果有具体的文章对象obj不为空调用文章模型的get_full_url方法获取前台URL
# lxy: 如果有具体的文章对象obj不为空调用文章模型的get_full_url方法获取前台URL
if obj:
url = obj.get_full_url()
return url
# 如果没有文章对象(如在列表页顶部的“查看站点”按钮),返回当前站点的域名
# lxy: 如果没有文章对象(如在列表页顶部的“查看站点”按钮),返回当前站点的域名
else:
# 从自定义工具模块导入获取当前站点的函数需确保djangoblog.utils存在
# lxy: 从自定义工具模块导入获取当前站点的函数需确保djangoblog.utils存在
from djangoblog.utils import get_current_site
# 获取当前站点的域名如www.example.com
# lxy: 获取当前站点的域名如www.example.com
site = get_current_site().domain
return site
# 自定义Tag模型标签模型的Admin配置类假设Tag模型在models.py中定义
# lxy: 自定义Tag模型标签模型的Admin配置类假设Tag模型在models.py中定义
class TagAdmin(admin.ModelAdmin):
# 后台添加/编辑标签时,隐藏的字段(不允许手动修改)
# lxy: 后台添加/编辑标签时,隐藏的字段(不允许手动修改)
exclude = ('slug', 'last_mod_time', 'creation_time')
# 自定义Category模型分类模型的Admin配置类假设Category模型在models.py中定义
# lxy: 自定义Category模型分类模型的Admin配置类假设Category模型在models.py中定义
class CategoryAdmin(admin.ModelAdmin):
# 后台分类列表页显示的字段:分类名称、父分类、排序索引
# lxy: 后台分类列表页显示的字段:分类名称、父分类、排序索引
list_display = ('name', 'parent_category', 'index')
# 后台添加/编辑分类时,隐藏的字段(不允许手动修改)
# lxy: 后台添加/编辑分类时,隐藏的字段(不允许手动修改)
exclude = ('slug', 'last_mod_time', 'creation_time')
# 自定义Links模型友情链接模型的Admin配置类假设Links模型在models.py中定义
# lxy: 自定义Links模型友情链接模型的Admin配置类假设Links模型在models.py中定义
class LinksAdmin(admin.ModelAdmin):
# 后台添加/编辑友情链接时,隐藏的字段(不允许手动修改)
# lxy: 后台添加/编辑友情链接时,隐藏的字段(不允许手动修改)
exclude = ('last_mod_time', 'creation_time')
# 自定义SideBar模型侧边栏模型的Admin配置类假设SideBar模型在models.py中定义
# lxy: 自定义SideBar模型侧边栏模型的Admin配置类假设SideBar模型在models.py中定义
class SideBarAdmin(admin.ModelAdmin):
# 后台侧边栏列表页显示的字段:名称、内容、是否启用、排序序号
# lxy: 后台侧边栏列表页显示的字段:名称、内容、是否启用、排序序号
list_display = ('name', 'content', 'is_enable', 'sequence')
# 后台添加/编辑侧边栏时,隐藏的字段(不允许手动修改)
# lxy: 后台添加/编辑侧边栏时,隐藏的字段(不允许手动修改)
exclude = ('last_mod_time', 'creation_time')
# 自定义BlogSettings模型博客设置模型的Admin配置类假设BlogSettings模型在models.py中定义
# lxy: 自定义BlogSettings模型博客设置模型的Admin配置类假设BlogSettings模型在models.py中定义
class BlogSettingsAdmin(admin.ModelAdmin):
# 空配置使用ModelAdmin的默认功能如需自定义可后续添加字段
# lxy: 空配置使用ModelAdmin的默认功能如需自定义可后续添加字段
pass

@ -1,45 +1,45 @@
# 导入Python内置time模块用于生成唯一ID时间戳毫秒级
# lxy: 导入Python内置time模块用于生成唯一ID时间戳毫秒级
import time
# 导入Elasticsearch客户端模块用于直接操作Elasticsearch服务如创建管道、删除索引
# lxy: 导入Elasticsearch客户端模块用于直接操作Elasticsearch服务如创建管道、删除索引
import elasticsearch.client
# 导入Django配置模块用于读取项目中的Elasticsearch配置settings.py中
# lxy: 导入Django配置模块用于读取项目中的Elasticsearch配置settings.py中
from django.conf import settings
# 从elasticsearch-dsl库导入核心组件
# DocumentElasticsearch文档模型基类类似Django的Model
# InnerDoc嵌套文档基类用于存储结构化子数据如地理位置、用户代理信息
# 字段类型Date(日期)、Integer(整数)、Long(长整数)、Text(可分词文本)、Object(对象类型)、GeoPoint(地理坐标)、Keyword(不可分词文本)、Boolean(布尔值)
# lxy: 从elasticsearch-dsl库导入核心组件
# lxy: DocumentElasticsearch文档模型基类类似Django的Model
# lxy: InnerDoc嵌套文档基类用于存储结构化子数据如地理位置、用户代理信息
# lxy: 字段类型Date(日期)、Integer(整数)、Long(长整数)、Text(可分词文本)、Object(对象类型)、GeoPoint(地理坐标)、Keyword(不可分词文本)、Boolean(布尔值)
from elasticsearch_dsl import Document, InnerDoc, Date, Integer, Long, Text, Object, GeoPoint, Keyword, Boolean
# 导入elasticsearch-dsl的连接管理模块用于建立与Elasticsearch服务的连接
# lxy: 导入elasticsearch-dsl的连接管理模块用于建立与Elasticsearch服务的连接
from elasticsearch_dsl.connections import connections
# 从当前应用blog的models.py导入Article模型用于将文章数据同步到Elasticsearch
# lxy: 从当前应用blog的models.py导入Article模型用于将文章数据同步到Elasticsearch
from blog.models import Article
# 判断项目是否启用Elasticsearch检查settings.py中是否配置了ELASTICSEARCH_DSL
# lxy: 判断项目是否启用Elasticsearch检查settings.py中是否配置了ELASTICSEARCH_DSL
ELASTICSEARCH_ENABLED = hasattr(settings, 'ELASTICSEARCH_DSL')
# 如果启用了Elasticsearch执行以下初始化操作
# lxy: 如果启用了Elasticsearch执行以下初始化操作
if ELASTICSEARCH_ENABLED:
# 建立与Elasticsearch服务的连接从settings中读取配置的主机地址如['http://localhost:9200']
# lxy: 建立与Elasticsearch服务的连接从settings中读取配置的主机地址如['http://localhost:9200']
connections.create_connection(
hosts=[settings.ELASTICSEARCH_DSL['default']['hosts']])
# 导入Elasticsearch原生客户端用于执行更底层的操作如创建索引、删除索引
# lxy: 导入Elasticsearch原生客户端用于执行更底层的操作如创建索引、删除索引
from elasticsearch import Elasticsearch
# 初始化Elasticsearch原生客户端传入服务地址
# lxy: 初始化Elasticsearch原生客户端传入服务地址
es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
# 导入Elasticsearch的IngestClient数据处理管道客户端用于创建数据预处理管道
# lxy: 导入Elasticsearch的IngestClient数据处理管道客户端用于创建数据预处理管道
from elasticsearch.client import IngestClient
# 初始化IngestClient绑定到上面创建的Elasticsearch客户端
# lxy: 初始化IngestClient绑定到上面创建的Elasticsearch客户端
c = IngestClient(es)
try:
# 尝试获取名为'geoip'的数据处理管道用于解析IP地址对应的地理位置
# lxy: 尝试获取名为'geoip'的数据处理管道用于解析IP地址对应的地理位置
c.get_pipeline('geoip')
# 如果管道不存在捕获NotFoundError异常则创建该管道
# lxy: 如果管道不存在捕获NotFoundError异常则创建该管道
except elasticsearch.exceptions.NotFoundError:
# 创建'geoip'管道定义数据处理逻辑通过geoip处理器解析IP地址
# lxy: 创建'geoip'管道定义数据处理逻辑通过geoip处理器解析IP地址
c.put_pipeline('geoip', body='''{
"description" : "Add geoip info", // 管道描述添加地理位置信息
"processors" : [ // 处理器列表定义数据处理步骤
@ -52,7 +52,7 @@ if ELASTICSEARCH_ENABLED:
}''')
# 定义GeoIp嵌套文档类InnerDoc存储IP地址解析后的地理位置信息
# lxy: 定义GeoIp嵌套文档类InnerDoc存储IP地址解析后的地理位置信息
class GeoIp(InnerDoc):
continent_name = Keyword() # 洲名Keyword类型不可分词适合精确查询/排序)
country_iso_code = Keyword() # 国家ISO代码如CN、USKeyword类型
@ -60,25 +60,25 @@ class GeoIp(InnerDoc):
location = GeoPoint() # 地理坐标经纬度GeoPoint类型支持地理位置查询
# 定义UserAgentBrowser嵌套文档类存储用户代理UA中的浏览器信息
# lxy: 定义UserAgentBrowser嵌套文档类存储用户代理UA中的浏览器信息
class UserAgentBrowser(InnerDoc):
Family = Keyword() # 浏览器家族如Chrome、FirefoxKeyword类型
Version = Keyword() # 浏览器版本如120.0Keyword类型
# 定义UserAgentOS嵌套文档类存储用户代理中的操作系统信息继承自UserAgentBrowser结构一致
# lxy: 定义UserAgentOS嵌套文档类存储用户代理中的操作系统信息继承自UserAgentBrowser结构一致
class UserAgentOS(UserAgentBrowser):
pass # 直接继承父类字段,无需额外定义
# 定义UserAgentDevice嵌套文档类存储用户代理中的设备信息
# lxy: 定义UserAgentDevice嵌套文档类存储用户代理中的设备信息
class UserAgentDevice(InnerDoc):
Family = Keyword() # 设备家族如iPhone、WindowsKeyword类型
Brand = Keyword() # 设备品牌如Apple、HuaweiKeyword类型
Model = Keyword() # 设备型号如iPhone 15Keyword类型
# 定义UserAgent嵌套文档类存储完整的用户代理信息包含浏览器、OS、设备
# lxy: 定义UserAgent嵌套文档类存储完整的用户代理信息包含浏览器、OS、设备
class UserAgent(InnerDoc):
browser = Object(UserAgentBrowser, required=False) # 浏览器信息Object类型关联UserAgentBrowser
os = Object(UserAgentOS, required=False) # 操作系统信息Object类型关联UserAgentOS
@ -87,7 +87,7 @@ class UserAgent(InnerDoc):
is_bot = Boolean() # 是否为爬虫Boolean类型true/false
# 定义ElapsedTimeDocument文档类Elasticsearch中的"性能监控"文档模型(记录请求耗时、访问信息)
# lxy: 定义ElapsedTimeDocument文档类Elasticsearch中的"性能监控"文档模型(记录请求耗时、访问信息)
class ElapsedTimeDocument(Document):
url = Keyword() # 访问URLKeyword类型精确匹配不分词
time_taken = Long() # 请求耗时毫秒Long类型支持大范围数值存储
@ -96,7 +96,7 @@ class ElapsedTimeDocument(Document):
geoip = Object(GeoIp, required=False) # 地理位置信息Object类型关联GeoIp嵌套文档非必填
useragent = Object(UserAgent, required=False) # 用户代理信息Object类型关联UserAgent嵌套文档非必填
# 定义文档对应的Elasticsearch索引配置
# lxy: 定义文档对应的Elasticsearch索引配置
class Index:
name = 'performance' # 索引名称Elasticsearch中存储性能数据的索引名
settings = { # 索引设置
@ -104,61 +104,61 @@ class ElapsedTimeDocument(Document):
"number_of_replicas": 0 # 副本数0个开发/小型场景无需副本,节省资源)
}
# 定义文档元数据兼容Elasticsearch旧版本doc_type在7.x后已废弃此处保留兼容
# lxy: 定义文档元数据兼容Elasticsearch旧版本doc_type在7.x后已废弃此处保留兼容
class Meta:
doc_type = 'ElapsedTime' # 文档类型:标识索引中的文档类别
# 定义ElaspedTimeDocumentManager类ElapsedTimeDocument的管理类封装索引创建、数据插入等操作
# lxy: 定义ElaspedTimeDocumentManager类ElapsedTimeDocument的管理类封装索引创建、数据插入等操作
class ElaspedTimeDocumentManager:
# 静态方法:创建性能监控索引(如果不存在)
# lxy: 静态方法:创建性能监控索引(如果不存在)
@staticmethod
def build_index():
# 导入Elasticsearch原生客户端
# lxy: 导入Elasticsearch原生客户端
from elasticsearch import Elasticsearch
# 初始化客户端读取settings中的Elasticsearch地址
# lxy: 初始化客户端读取settings中的Elasticsearch地址
client = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
# 检查名为'performance'的索引是否已存在
# lxy: 检查名为'performance'的索引是否已存在
res = client.indices.exists(index="performance")
# 如果索引不存在初始化ElapsedTimeDocument创建索引及映射
# lxy: 如果索引不存在初始化ElapsedTimeDocument创建索引及映射
if not res:
ElapsedTimeDocument.init()
# 静态方法:删除性能监控索引
# lxy: 静态方法:删除性能监控索引
@staticmethod
def delete_index():
from elasticsearch import Elasticsearch
es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
# 删除'performance'索引忽略400请求错误和404索引不存在异常
# lxy: 删除'performance'索引忽略400请求错误和404索引不存在异常
es.indices.delete(index='performance', ignore=[400, 404])
# 静态方法:创建性能监控文档(插入一条访问耗时记录)
# lxy: 静态方法:创建性能监控文档(插入一条访问耗时记录)
@staticmethod
def create(url, time_taken, log_datetime, useragent, ip):
# 确保索引已创建调用build_index方法
# lxy: 确保索引已创建调用build_index方法
ElaspedTimeDocumentManager.build_index()
# 初始化UserAgent嵌套文档对象
# lxy: 初始化UserAgent嵌套文档对象
ua = UserAgent()
# 赋值浏览器信息从传入的useragent对象中提取浏览器家族和版本
# lxy: 赋值浏览器信息从传入的useragent对象中提取浏览器家族和版本
ua.browser = UserAgentBrowser()
ua.browser.Family = useragent.browser.family
ua.browser.Version = useragent.browser.version_string
# 赋值操作系统信息从传入的useragent对象中提取OS家族和版本
# lxy: 赋值操作系统信息从传入的useragent对象中提取OS家族和版本
ua.os = UserAgentOS()
ua.os.Family = useragent.os.family
ua.os.Version = useragent.os.version_string
# 赋值设备信息从传入的useragent对象中提取设备家族、品牌、型号
# lxy: 赋值设备信息从传入的useragent对象中提取设备家族、品牌、型号
ua.device = UserAgentDevice()
ua.device.Family = useragent.device.family
ua.device.Brand = useragent.device.brand
ua.device.Model = useragent.device.model
# 赋值完整UA字符串和是否为爬虫的标识
# lxy: 赋值完整UA字符串和是否为爬虫的标识
ua.string = useragent.ua_string
ua.is_bot = useragent.is_bot
# 初始化ElapsedTimeDocument文档对象设置字段值
# lxy: 初始化ElapsedTimeDocument文档对象设置字段值
doc = ElapsedTimeDocument(
meta={
'id': int(round(time.time() * 1000)) # 文档ID毫秒级时间戳确保唯一
@ -168,27 +168,27 @@ class ElaspedTimeDocumentManager:
log_datetime=log_datetime,# 记录时间
useragent=ua, # 用户代理信息(嵌套文档)
ip=ip) # 访问IP
# 保存文档到Elasticsearch并指定使用'geoip'管道预处理解析IP地址
# lxy: 保存文档到Elasticsearch并指定使用'geoip'管道预处理解析IP地址
doc.save(pipeline="geoip")
# 定义ArticleDocument文档类Elasticsearch中的"文章"文档模型(用于文章搜索)
# lxy: 定义ArticleDocument文档类Elasticsearch中的"文章"文档模型(用于文章搜索)
class ArticleDocument(Document):
# 文章内容Text类型使用ik_max_word分词器分词更细适合全文搜索搜索时用ik_smart分词更粗提升效率
# lxy: 文章内容Text类型使用ik_max_word分词器分词更细适合全文搜索搜索时用ik_smart分词更粗提升效率
body = Text(analyzer='ik_max_word', search_analyzer='ik_smart')
# 文章标题:同上,支持中文分词搜索
# lxy: 文章标题:同上,支持中文分词搜索
title = Text(analyzer='ik_max_word', search_analyzer='ik_smart')
# 作者信息Object类型包含昵称可分词和ID整数
# lxy: 作者信息Object类型包含昵称可分词和ID整数
author = Object(properties={
'nickname': Text(analyzer='ik_max_word', search_analyzer='ik_smart'),
'id': Integer()
})
# 分类信息Object类型包含分类名称可分词和ID整数
# lxy: 分类信息Object类型包含分类名称可分词和ID整数
category = Object(properties={
'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'),
'id': Integer()
})
# 标签信息Object类型数组每个标签包含名称可分词和ID整数
# lxy: 标签信息Object类型数组每个标签包含名称可分词和ID整数
tags = Object(properties={
'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'),
'id': Integer()
@ -201,7 +201,7 @@ class ArticleDocument(Document):
views = Integer() # 浏览量Integer类型支持数值排序
article_order = Integer() # 排序权重Integer类型用于自定义文章排序
# 定义文档对应的Elasticsearch索引配置
# lxy: 定义文档对应的Elasticsearch索引配置
class Index:
name = 'blog' # 索引名称:存储文章数据的索引名
settings = { # 索引设置
@ -209,31 +209,31 @@ class ArticleDocument(Document):
"number_of_replicas": 0 # 副本数0个开发/小型场景节省资源)
}
# 文档元数据兼容旧版本Elasticsearch的doc_type
# lxy: 文档元数据兼容旧版本Elasticsearch的doc_type
class Meta:
doc_type = 'Article' # 文档类型:标识为文章类文档
# 定义ArticleDocumentManager类ArticleDocument的管理类封装文章索引的创建、重建、更新等操作
# lxy: 定义ArticleDocumentManager类ArticleDocument的管理类封装文章索引的创建、重建、更新等操作
class ArticleDocumentManager():
# 构造方法:实例化管理类时自动创建文章索引(如果不存在)
# lxy: 构造方法:实例化管理类时自动创建文章索引(如果不存在)
def __init__(self):
self.create_index()
# 实例方法创建文章索引调用ArticleDocument的init方法生成索引和字段映射
# lxy: 实例方法创建文章索引调用ArticleDocument的init方法生成索引和字段映射
def create_index(self):
ArticleDocument.init()
# 实例方法:删除文章索引
# lxy: 实例方法:删除文章索引
def delete_index(self):
from elasticsearch import Elasticsearch
es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
# 删除'blog'索引忽略400和404异常
# lxy: 删除'blog'索引忽略400和404异常
es.indices.delete(index='blog', ignore=[400, 404])
# 实例方法将Django的Article模型对象列表转换为Elasticsearch的ArticleDocument列表
# lxy: 实例方法将Django的Article模型对象列表转换为Elasticsearch的ArticleDocument列表
def convert_to_doc(self, articles):
# 列表推导式遍历每篇文章构建对应的ArticleDocument
# lxy: 列表推导式遍历每篇文章构建对应的ArticleDocument
return [
ArticleDocument(
meta={'id': article.id}, # 文档ID与Django Article模型ID一致便于关联
@ -258,20 +258,20 @@ class ArticleDocumentManager():
article_order=article.article_order # 排序权重
) for article in articles]
# 实例方法重建文章索引全量同步文章数据到Elasticsearch
# lxy: 实例方法重建文章索引全量同步文章数据到Elasticsearch
def rebuild(self, articles=None):
# 确保索引已创建(初始化索引和映射)
# lxy: 确保索引已创建(初始化索引和映射)
ArticleDocument.init()
# 如果传入了articles参数则同步指定文章否则同步所有文章Article.objects.all()
# lxy: 如果传入了articles参数则同步指定文章否则同步所有文章Article.objects.all()
articles = articles if articles else Article.objects.all()
# 将Django Article对象转换为Elasticsearch文档列表
# lxy: 将Django Article对象转换为Elasticsearch文档列表
docs = self.convert_to_doc(articles)
# 遍历文档列表逐个保存到Elasticsearch
# lxy: 遍历文档列表逐个保存到Elasticsearch
for doc in docs:
doc.save()
# 实例方法批量更新Elasticsearch中的文章文档
# lxy: 实例方法批量更新Elasticsearch中的文章文档
def update_docs(self, docs):
# 遍历文档列表,逐个保存(已存在的文档会执行更新操作)
# lxy: 遍历文档列表,逐个保存(已存在的文档会执行更新操作)
for doc in docs:
doc.save()

@ -1,38 +1,38 @@
# 导入Python内置logging模块用于记录搜索相关日志如搜索关键词
# lxy: 导入Python内置logging模块用于记录搜索相关日志如搜索关键词
import logging
# 导入Django表单基础模块用于创建自定义表单字段
# lxy: 导入Django表单基础模块用于创建自定义表单字段
from django import forms
# 从Haystack库导入基础搜索表单类SearchFormHaystack是Django的搜索引擎集成框架
# lxy: 从Haystack库导入基础搜索表单类SearchFormHaystack是Django的搜索引擎集成框架
from haystack.forms import SearchForm
# 创建日志记录器日志名称与当前模块一致__name__便于定位日志来源区分其他模块日志
# lxy: 创建日志记录器日志名称与当前模块一致__name__便于定位日志来源区分其他模块日志
logger = logging.getLogger(__name__)
# 定义自定义搜索表单类BlogSearchForm继承自Haystack提供的SearchForm基础搜索表单
# 作用扩展Haystack默认搜索表单添加自定义字段和搜索逻辑
# lxy: 定义自定义搜索表单类BlogSearchForm继承自Haystack提供的SearchForm基础搜索表单
# lxy: 作用扩展Haystack默认搜索表单添加自定义字段和搜索逻辑
class BlogSearchForm(SearchForm):
# 定义搜索输入字段querydata搜索关键词字段
# required=True表示该字段为必填项用户必须输入关键词才能提交搜索
# CharField单行文本输入框适合接收搜索关键词
# lxy: 定义搜索输入字段querydata搜索关键词字段
# lxy: required=True表示该字段为必填项用户必须输入关键词才能提交搜索
# lxy: CharField单行文本输入框适合接收搜索关键词
querydata = forms.CharField(required=True)
# 重写父类的search方法自定义搜索逻辑保留父类核心功能添加日志记录
# lxy: 重写父类的search方法自定义搜索逻辑保留父类核心功能添加日志记录
def search(self):
# 1. 调用父类SearchForm的search方法执行Haystack默认搜索流程
# 父类会自动处理索引查询、关键词匹配等核心逻辑返回搜索结果集SearchQuerySet对象
# lxy: 1. 调用父类SearchForm的search方法执行Haystack默认搜索流程
# lxy: 父类会自动处理索引查询、关键词匹配等核心逻辑返回搜索结果集SearchQuerySet对象
datas = super(BlogSearchForm, self).search()
# 2. 验证表单数据是否合法根据字段定义的规则如required=True
# lxy: 2. 验证表单数据是否合法根据字段定义的规则如required=True
if not self.is_valid():
# 若表单数据不合法如未输入关键词调用父类的no_query_found方法返回默认空结果
# lxy: 若表单数据不合法如未输入关键词调用父类的no_query_found方法返回默认空结果
return self.no_query_found()
# 3. 若表单验证通过获取清理后的搜索关键词cleaned_data是Django表单验证后的安全数据字典
# lxy: 3. 若表单验证通过获取清理后的搜索关键词cleaned_data是Django表单验证后的安全数据字典
if self.cleaned_data['querydata']:
# 记录搜索日志:将用户输入的关键词写入日志(便于统计热门搜索、排查问题)
# lxy: 记录搜索日志:将用户输入的关键词写入日志(便于统计热门搜索、排查问题)
logger.info(self.cleaned_data['querydata'])
# 4. 返回搜索结果集datas该结果集会传递给搜索结果页面模板进行渲染
# lxy: 4. 返回搜索结果集datas该结果集会传递给搜索结果页面模板进行渲染
return datas

@ -4,7 +4,7 @@ from blog.documents import ElapsedTimeDocument, ArticleDocumentManager, ElaspedT
ELASTICSEARCH_ENABLED
# TODO 参数化
# lxy: TODO 参数化
class Command(BaseCommand):
help = 'build search index'

@ -3,7 +3,7 @@ from django.core.management.base import BaseCommand
from blog.models import Tag, Category
# TODO 参数化
# lxy: TODO 参数化
class Command(BaseCommand):
help = 'build search words'

@ -1,79 +1,79 @@
# 导入Python内置logging模块用于记录中间件运行过程中的日志如错误信息
# lxy: 导入Python内置logging模块用于记录中间件运行过程中的日志如错误信息
import logging
# 导入Python内置time模块用于计算请求处理耗时页面加载时间
# lxy: 导入Python内置time模块用于计算请求处理耗时页面加载时间
import time
# 导入ipware库的get_client_ip函数用于获取请求客户端的真实IP地址兼容多种部署场景
# lxy: 导入ipware库的get_client_ip函数用于获取请求客户端的真实IP地址兼容多种部署场景
from ipware import get_client_ip
# 导入user_agents库的parse函数用于解析用户代理UA字符串提取浏览器、设备、系统信息
# lxy: 导入user_agents库的parse函数用于解析用户代理UA字符串提取浏览器、设备、系统信息
from user_agents import parse
# 从当前应用的documents模块导入
# 1. ELASTICSEARCH_ENABLED判断项目是否启用Elasticsearch之前定义的全局变量
# 2. ElaspedTimeDocumentManager性能监控文档管理类用于将耗时数据存入Elasticsearch
# lxy: 从当前应用的documents模块导入
# lxy: 1. ELASTICSEARCH_ENABLED判断项目是否启用Elasticsearch之前定义的全局变量
# lxy: 2. ElaspedTimeDocumentManager性能监控文档管理类用于将耗时数据存入Elasticsearch
from blog.documents import ELASTICSEARCH_ENABLED, ElaspedTimeDocumentManager
# 创建日志记录器日志名称与当前模块一致__name__便于定位日志来源区分其他模块日志
# lxy: 创建日志记录器日志名称与当前模块一致__name__便于定位日志来源区分其他模块日志
logger = logging.getLogger(__name__)
# 定义自定义中间件类OnlineMiddleware遵循Django中间件接口规范
# 作用1. 计算请求处理耗时页面加载时间2. 记录访问IP、设备信息到Elasticsearch3. 替换页面中的加载时间占位符
# lxy: 定义自定义中间件类OnlineMiddleware遵循Django中间件接口规范
# lxy: 作用1. 计算请求处理耗时页面加载时间2. 记录访问IP、设备信息到Elasticsearch3. 替换页面中的加载时间占位符
class OnlineMiddleware(object):
# 中间件初始化方法接收get_response参数Django 1.10+中间件必需参数,代表后续中间件/视图的响应流程)
# lxy: 中间件初始化方法接收get_response参数Django 1.10+中间件必需参数,代表后续中间件/视图的响应流程)
def __init__(self, get_response=None):
# 保存get_response到实例属性后续在__call__方法中调用确保请求流程继续向下执行
# lxy: 保存get_response到实例属性后续在__call__方法中调用确保请求流程继续向下执行
self.get_response = get_response
# 调用父类object的初始化方法确保基础类功能正常Python 2/3兼容写法
# lxy: 调用父类object的初始化方法确保基础类功能正常Python 2/3兼容写法
super().__init__()
# 中间件核心执行方法,处理每个请求的入口和出口(请求到达时执行前半部分,响应返回时执行后半部分)
# lxy: 中间件核心执行方法,处理每个请求的入口和出口(请求到达时执行前半部分,响应返回时执行后半部分)
def __call__(self, request):
''' page render time ''' # 注释:该方法用于计算页面渲染耗时
# 记录请求开始时间(时间戳,单位:秒),作为耗时计算的起始点
# lxy: 记录请求开始时间(时间戳,单位:秒),作为耗时计算的起始点
start_time = time.time()
# 调用后续中间件/视图函数获取响应对象response此时请求已完成业务处理
# lxy: 调用后续中间件/视图函数获取响应对象response此时请求已完成业务处理
response = self.get_response(request)
# 从请求的META信息中获取用户代理UA字符串
# HTTP_USER_AGENT是请求头中的字段包含浏览器、设备、系统等信息默认值为空字符串
# lxy: 从请求的META信息中获取用户代理UA字符串
# lxy: HTTP_USER_AGENT是请求头中的字段包含浏览器、设备、系统等信息默认值为空字符串
http_user_agent = request.META.get('HTTP_USER_AGENT', '')
# 调用get_client_ip函数获取客户端真实IP
# 返回值为元组ip地址, 是否为代理IP此处仅取IP地址_忽略代理标记
# lxy: 调用get_client_ip函数获取客户端真实IP
# lxy: 返回值为元组ip地址, 是否为代理IP此处仅取IP地址_忽略代理标记
ip, _ = get_client_ip(request)
# 解析UA字符串调用parse函数将原始UA字符串转换为结构化对象可通过属性获取浏览器/设备/系统信息)
# lxy: 解析UA字符串调用parse函数将原始UA字符串转换为结构化对象可通过属性获取浏览器/设备/系统信息)
user_agent = parse(http_user_agent)
# 判断响应是否为非流式响应(流式响应如文件下载,无需处理加载时间和替换占位符)
# lxy: 判断响应是否为非流式响应(流式响应如文件下载,无需处理加载时间和替换占位符)
if not response.streaming:
try:
# 计算请求处理总耗时:当前时间 - 开始时间(单位:秒)
# lxy: 计算请求处理总耗时:当前时间 - 开始时间(单位:秒)
cast_time = time.time() - start_time
# 如果启用了Elasticsearch将性能数据存入Elasticsearch
# lxy: 如果启用了Elasticsearch将性能数据存入Elasticsearch
if ELASTICSEARCH_ENABLED:
# 耗时转换为毫秒保留2位小数更符合性能监控的常用单位
# lxy: 耗时转换为毫秒保留2位小数更符合性能监控的常用单位
time_taken = round((cast_time) * 1000, 2)
# 获取请求的路径(如"/article/1/"作为性能记录的URL标识
# lxy: 获取请求的路径(如"/article/1/"作为性能记录的URL标识
url = request.path
# 导入Django的timezone模块延迟导入避免循环导入问题用于获取当前时间
# lxy: 导入Django的timezone模块延迟导入避免循环导入问题用于获取当前时间
from django.utils import timezone
# 调用ElaspedTimeDocumentManager的create方法插入性能记录到Elasticsearch
# 包含URL、耗时、记录时间、用户代理信息、IP地址
# lxy: 调用ElaspedTimeDocumentManager的create方法插入性能记录到Elasticsearch
# lxy: 包含URL、耗时、记录时间、用户代理信息、IP地址
ElaspedTimeDocumentManager.create(
url=url,
time_taken=time_taken,
log_datetime=timezone.now(),
useragent=user_agent,
ip=ip)
# 替换响应内容中的占位符:
# 将页面中的'<!!LOAD_TIMES!!>'字符串替换为实际耗时保留前5个字符如"0.321"
# 注意response.content是字节类型需用str.encode将字符串耗时转换为字节
# lxy: 替换响应内容中的占位符:
# lxy: 将页面中的'<!!LOAD_TIMES!!>'字符串替换为实际耗时保留前5个字符如"0.321"
# lxy: 注意response.content是字节类型需用str.encode将字符串耗时转换为字节
response.content = response.content.replace(
b'<!!LOAD_TIMES!!>', str.encode(str(cast_time)[:5]))
# 捕获所有异常,避免中间件报错导致响应失败
# lxy: 捕获所有异常,避免中间件报错导致响应失败
except Exception as e:
# 记录异常日志将错误信息写入日志便于后续排查问题如Elasticsearch连接失败、占位符替换失败
# lxy: 记录异常日志将错误信息写入日志便于后续排查问题如Elasticsearch连接失败、占位符替换失败
logger.error("Error OnlineMiddleware: %s" % e)
# 返回处理后的响应对象,最终返回给客户端
# lxy: 返回处理后的响应对象,最终返回给客户端
return response

@ -1,4 +1,4 @@
# Generated by Django 4.1.7 on 2023-03-02 07:14
# lxy: Generated by Django 4.1.7 on 2023-03-02 07:14
from django.conf import settings
from django.db import migrations, models

@ -1,4 +1,4 @@
# Generated by Django 4.1.7 on 2023-03-29 06:08
# lxy: Generated by Django 4.1.7 on 2023-03-29 06:08
from django.db import migrations, models

@ -1,4 +1,4 @@
# Generated by Django 4.2.1 on 2023-05-09 07:45
# lxy: Generated by Django 4.2.1 on 2023-05-09 07:45
from django.db import migrations, models

@ -1,4 +1,4 @@
# Generated by Django 4.2.1 on 2023-05-09 07:51
# lxy: Generated by Django 4.2.1 on 2023-05-09 07:51
from django.db import migrations

@ -1,4 +1,4 @@
# Generated by Django 4.2.5 on 2023-09-06 13:13
# lxy: Generated by Django 4.2.5 on 2023-09-06 13:13
from django.conf import settings
from django.db import migrations, models

@ -1,4 +1,4 @@
# Generated by Django 4.2.7 on 2024-01-26 02:41
# lxy: Generated by Django 4.2.7 on 2024-01-26 02:41
from django.db import migrations

@ -1,38 +1,38 @@
# 导入Python内置logging模块用于记录模型操作相关日志如缓存命中/设置、数据验证错误)
# lxy: 导入Python内置logging模块用于记录模型操作相关日志如缓存命中/设置、数据验证错误)
import logging
# 从abc模块导入abstractmethod装饰器用于定义抽象方法强制子类实现
# lxy: 从abc模块导入abstractmethod装饰器用于定义抽象方法强制子类实现
from abc import abstractmethod
# 导入Django配置模块用于获取项目配置如AUTH_USER_MODEL
# lxy: 导入Django配置模块用于获取项目配置如AUTH_USER_MODEL
from django.conf import settings
# 导入Django数据验证异常类用于自定义数据验证逻辑如博客配置唯一性校验
# lxy: 导入Django数据验证异常类用于自定义数据验证逻辑如博客配置唯一性校验
from django.core.exceptions import ValidationError
# 导入Django模型核心模块用于定义数据模型对应数据库表
# lxy: 导入Django模型核心模块用于定义数据模型对应数据库表
from django.db import models
# 导入Django URL反向解析模块用于生成模型的绝对URL
# lxy: 导入Django URL反向解析模块用于生成模型的绝对URL
from django.urls import reverse
# 导入Django时区工具用于处理时间字段确保时间戳一致性
# lxy: 导入Django时区工具用于处理时间字段确保时间戳一致性
from django.utils.timezone import now
# 导入Django国际化翻译工具用于模型字段/选项的多语言支持
# lxy: 导入Django国际化翻译工具用于模型字段/选项的多语言支持
from django.utils.translation import gettext_lazy as _
# 导入MDTextField字段来自mdeditor库用于支持Markdown格式的富文本编辑
# lxy: 导入MDTextField字段来自mdeditor库用于支持Markdown格式的富文本编辑
from mdeditor.fields import MDTextField
# 导入uuslug库的slugify函数用于将中文标题/名称转换为URL友好的slug如"我的博客"→"wo-de-bo-ke"
# lxy: 导入uuslug库的slugify函数用于将中文标题/名称转换为URL友好的slug如"我的博客"→"wo-de-bo-ke"
from uuslug import slugify
# 从自定义工具模块导入缓存相关工具:
# 1. cache_decorator缓存装饰器用于缓存函数返回结果
# 2. cache缓存操作对象用于直接读写缓存
# lxy: 从自定义工具模块导入缓存相关工具:
# lxy: 1. cache_decorator缓存装饰器用于缓存函数返回结果
# lxy: 2. cache缓存操作对象用于直接读写缓存
from djangoblog.utils import cache_decorator, cache
# 从自定义工具模块导入获取当前站点信息的函数用于生成完整URL
# lxy: 从自定义工具模块导入获取当前站点信息的函数用于生成完整URL
from djangoblog.utils import get_current_site
# 创建日志记录器日志名称与当前模块一致__name__便于定位日志来源
# lxy: 创建日志记录器日志名称与当前模块一致__name__便于定位日志来源
logger = logging.getLogger(__name__)
# 定义链接显示类型枚举类LinkShowType继承自Django的TextChoices枚举基类
# 作用:规范友情链接的显示位置选项,避免硬编码字符串
# lxy: 定义链接显示类型枚举类LinkShowType继承自Django的TextChoices枚举基类
# lxy: 作用:规范友情链接的显示位置选项,避免硬编码字符串
class LinkShowType(models.TextChoices):
I = ('i', _('index')) # 首页显示
L = ('l', _('list')) # 列表页显示
@ -41,145 +41,145 @@ class LinkShowType(models.TextChoices):
S = ('s', _('slide')) # 幻灯片区域显示
# 定义抽象基础模型类BaseModel继承自Django的models.Model
# 作用封装所有模型共有的字段和方法如创建时间、修改时间、URL生成避免代码重复
# abstract=TrueMeta类中表示该模型为抽象模型不会生成数据库表仅用于被子类继承
# lxy: 定义抽象基础模型类BaseModel继承自Django的models.Model
# lxy: 作用封装所有模型共有的字段和方法如创建时间、修改时间、URL生成避免代码重复
# lxy: abstract=TrueMeta类中表示该模型为抽象模型不会生成数据库表仅用于被子类继承
class BaseModel(models.Model):
# 主键ID自增整数类型Django默认主键此处显式定义以统一规范
# lxy: 主键ID自增整数类型Django默认主键此处显式定义以统一规范
id = models.AutoField(primary_key=True)
# 创建时间DateTimeField类型默认值为当前时间now()支持国际化显示_('creation time')
# lxy: 创建时间DateTimeField类型默认值为当前时间now()支持国际化显示_('creation time')
creation_time = models.DateTimeField(_('creation time'), default=now)
# 最后修改时间DateTimeField类型默认值为当前时间用于记录数据更新时间
# lxy: 最后修改时间DateTimeField类型默认值为当前时间用于记录数据更新时间
last_modify_time = models.DateTimeField(_('modify time'), default=now)
# 重写save方法扩展保存逻辑处理slug生成和浏览量更新优化
# lxy: 重写save方法扩展保存逻辑处理slug生成和浏览量更新优化
def save(self, *args, **kwargs):
# 判断是否为Article模型的浏览量更新操作
# 1. 实例是Article类的实例
# 2. save方法传入了update_fields参数
# 3. 仅更新views字段浏览量
# lxy: 判断是否为Article模型的浏览量更新操作
# lxy: 1. 实例是Article类的实例
# lxy: 2. save方法传入了update_fields参数
# lxy: 3. 仅更新views字段浏览量
is_update_views = isinstance(
self,
Article) and 'update_fields' in kwargs and kwargs['update_fields'] == ['views']
# 如果是浏览量单独更新直接执行SQL更新避免触发完整save流程提升性能
# lxy: 如果是浏览量单独更新直接执行SQL更新避免触发完整save流程提升性能
if is_update_views:
Article.objects.filter(pk=self.pk).update(views=self.views)
# 非浏览量更新场景,执行正常保存逻辑
# lxy: 非浏览量更新场景,执行正常保存逻辑
else:
# 判断当前模型是否有slug字段需要生成URL友好标识的模型如Category、Tag
# lxy: 判断当前模型是否有slug字段需要生成URL友好标识的模型如Category、Tag
if 'slug' in self.__dict__:
# 确定slug的生成源优先取title字段如Article无则取name字段如Category、Tag
# lxy: 确定slug的生成源优先取title字段如Article无则取name字段如Category、Tag
slug = getattr(
self, 'title') if 'title' in self.__dict__ else getattr(
self, 'name')
# 调用slugify函数生成slug并赋值给当前实例的slug字段
# lxy: 调用slugify函数生成slug并赋值给当前实例的slug字段
setattr(self, 'slug', slugify(slug))
# 调用父类的save方法完成数据入库必须调用否则数据不会保存
# lxy: 调用父类的save方法完成数据入库必须调用否则数据不会保存
super().save(*args, **kwargs)
# 生成模型实例的完整URL含域名用于前端跳转、SEO等场景
# lxy: 生成模型实例的完整URL含域名用于前端跳转、SEO等场景
def get_full_url(self):
# 获取当前站点的域名(如"www.example.com"通过get_current_site工具函数
# lxy: 获取当前站点的域名(如"www.example.com"通过get_current_site工具函数
site = get_current_site().domain
# 拼接完整URL协议默认https+ 域名 + 实例的相对URL通过get_absolute_url获取
# lxy: 拼接完整URL协议默认https+ 域名 + 实例的相对URL通过get_absolute_url获取
url = "https://{site}{path}".format(site=site,
path=self.get_absolute_url())
return url
# 模型元数据配置
# lxy: 模型元数据配置
class Meta:
abstract = True # 标记为抽象模型,不生成数据库表
# 定义抽象方法get_absolute_url强制子类实现
# 作用每个具体模型必须提供自己的相对URL生成逻辑如文章详情页URL、分类页URL
# lxy: 定义抽象方法get_absolute_url强制子类实现
# lxy: 作用每个具体模型必须提供自己的相对URL生成逻辑如文章详情页URL、分类页URL
@abstractmethod
def get_absolute_url(self):
pass
# 定义文章模型Article继承自抽象基础模型BaseModel
# lxy: 定义文章模型Article继承自抽象基础模型BaseModel
class Article(BaseModel):
"""文章模型:存储博客文章/页面数据(如博客文章、关于页、联系页等)"""
# 文章状态选项:元组形式,每个元素为(存储值,显示文本),支持国际化
# lxy: 文章状态选项:元组形式,每个元素为(存储值,显示文本),支持国际化
STATUS_CHOICES = (
('d', _('Draft')), # 'd':草稿状态
('p', _('Published')),# 'p':已发布状态
)
# 评论状态选项:控制文章是否允许评论
# lxy: 评论状态选项:控制文章是否允许评论
COMMENT_STATUS = (
('o', _('Open')), # 'o':开放评论
('c', _('Close')), # 'c':关闭评论
)
# 文章类型选项:区分普通文章和独立页面
# lxy: 文章类型选项:区分普通文章和独立页面
TYPE = (
('a', _('Article')), # 'a':普通文章(如博客博文)
('p', _('Page')), # 'p':独立页面(如关于页、隐私政策页)
)
# 文章标题CharField类型最大长度200唯一约束避免重复标题
# lxy: 文章标题CharField类型最大长度200唯一约束避免重复标题
title = models.CharField(_('title'), max_length=200, unique=True)
# 文章内容MDTextField类型支持Markdown格式编辑富文本
# lxy: 文章内容MDTextField类型支持Markdown格式编辑富文本
body = MDTextField(_('body'))
# 发布时间DateTimeField类型必填默认值为当前时间用于控制文章发布时间点
# lxy: 发布时间DateTimeField类型必填默认值为当前时间用于控制文章发布时间点
pub_time = models.DateTimeField(
_('publish time'), blank=False, null=False, default=now)
# 文章状态CharField类型长度1可选值为STATUS_CHOICES默认已发布'p'
# lxy: 文章状态CharField类型长度1可选值为STATUS_CHOICES默认已发布'p'
status = models.CharField(
_('status'),
max_length=1,
choices=STATUS_CHOICES,
default='p')
# 评论状态CharField类型长度1可选值为COMMENT_STATUS默认开放评论'o'
# lxy: 评论状态CharField类型长度1可选值为COMMENT_STATUS默认开放评论'o'
comment_status = models.CharField(
_('comment status'),
max_length=1,
choices=COMMENT_STATUS,
default='o')
# 文章类型CharField类型长度1可选值为TYPE默认普通文章'a'
# lxy: 文章类型CharField类型长度1可选值为TYPE默认普通文章'a'
type = models.CharField(_('type'), max_length=1, choices=TYPE, default='a')
# 浏览量PositiveIntegerField类型仅允许非负整数默认0
# lxy: 浏览量PositiveIntegerField类型仅允许非负整数默认0
views = models.PositiveIntegerField(_('views'), default=0)
# 作者外键关联Django用户模型settings.AUTH_USER_MODEL兼容自定义用户模型
# on_delete=models.CASCADE用户被删除时关联的文章也会被删除
# lxy: 作者外键关联Django用户模型settings.AUTH_USER_MODEL兼容自定义用户模型
# lxy: on_delete=models.CASCADE用户被删除时关联的文章也会被删除
author = models.ForeignKey(
settings.AUTH_USER_MODEL,
verbose_name=_('author'),
blank=False,
null=False,
on_delete=models.CASCADE)
# 文章排序权重IntegerField类型默认0用于自定义文章显示顺序值越大越靠前
# lxy: 文章排序权重IntegerField类型默认0用于自定义文章显示顺序值越大越靠前
article_order = models.IntegerField(
_('order'), blank=False, null=False, default=0)
# 是否显示目录BooleanField类型默认False控制文章详情页是否显示TOC目录
# lxy: 是否显示目录BooleanField类型默认False控制文章详情页是否显示TOC目录
show_toc = models.BooleanField(_('show toc'), blank=False, null=False, default=False)
# 分类外键关联Category模型自应用内的分类模型
# on_delete=models.CASCADE分类被删除时关联的文章也会被删除
# lxy: 分类外键关联Category模型自应用内的分类模型
# lxy: on_delete=models.CASCADE分类被删除时关联的文章也会被删除
category = models.ForeignKey(
'Category',
verbose_name=_('category'),
on_delete=models.CASCADE,
blank=False,
null=False)
# 标签多对多关联Tag模型一篇文章可多个标签一个标签可关联多篇文章允许为空
# lxy: 标签多对多关联Tag模型一篇文章可多个标签一个标签可关联多篇文章允许为空
tags = models.ManyToManyField('Tag', verbose_name=_('tag'), blank=True)
# 辅助方法:返回文章内容字符串(用于需要直接获取纯文本内容的场景)
# lxy: 辅助方法:返回文章内容字符串(用于需要直接获取纯文本内容的场景)
def body_to_string(self):
return self.body
# 重写__str__方法后台管理界面和打印实例时显示文章标题友好显示
# lxy: 重写__str__方法后台管理界面和打印实例时显示文章标题友好显示
def __str__(self):
return self.title
# 模型元数据配置
# lxy: 模型元数据配置
class Meta:
ordering = ['-article_order', '-pub_time'] # 默认排序:先按排序权重降序,再按发布时间降序
verbose_name = _('article') # 模型单数显示名称(支持国际化)
verbose_name_plural = verbose_name # 模型复数显示名称(与单数一致)
get_latest_by = 'id' # 指定获取最新记录的字段按ID降序
# 实现抽象基类的get_absolute_url方法生成文章的相对URL
# lxy: 实现抽象基类的get_absolute_url方法生成文章的相对URL
def get_absolute_url(self):
# 反向解析'blog:detailbyid'路由传递文章ID、发布年月日作为URL参数
# lxy: 反向解析'blog:detailbyid'路由传递文章ID、发布年月日作为URL参数
return reverse('blog:detailbyid', kwargs={
'article_id': self.id,
'year': self.creation_time.year,
@ -187,104 +187,104 @@ class Article(BaseModel):
'day': self.creation_time.day
})
# 缓存装饰器缓存结果10小时60*60*10秒避免重复查询数据库
# lxy: 缓存装饰器缓存结果10小时60*60*10秒避免重复查询数据库
@cache_decorator(60 * 60 * 10)
# 获取文章分类的层级关系(如"技术→Python→Django"
# lxy: 获取文章分类的层级关系(如"技术→Python→Django"
def get_category_tree(self):
# 调用分类模型的get_category_tree方法获取当前文章分类的所有父级分类
# lxy: 调用分类模型的get_category_tree方法获取当前文章分类的所有父级分类
tree = self.category.get_category_tree()
# 转换为分类名称分类URL的元组列表用于前端显示分类面包屑
# lxy: 转换为分类名称分类URL的元组列表用于前端显示分类面包屑
names = list(map(lambda c: (c.name, c.get_absolute_url()), tree))
return names
# 重写save方法此处仅调用父类方法便于后续扩展自定义逻辑
# lxy: 重写save方法此处仅调用父类方法便于后续扩展自定义逻辑
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
# 文章浏览量递增方法:用于文章详情页访问时更新浏览量
# lxy: 文章浏览量递增方法:用于文章详情页访问时更新浏览量
def viewed(self):
self.views += 1 # 浏览量+1
# 仅更新views字段通过update_fields参数优化避免更新其他字段
# lxy: 仅更新views字段通过update_fields参数优化避免更新其他字段
self.save(update_fields=['views'])
# 获取文章的评论列表(已启用的评论)
# lxy: 获取文章的评论列表(已启用的评论)
def comment_list(self):
# 定义缓存键包含文章ID确保不同文章的评论缓存不冲突
# lxy: 定义缓存键包含文章ID确保不同文章的评论缓存不冲突
cache_key = 'article_comments_{id}'.format(id=self.id)
# 尝试从缓存获取评论列表
# lxy: 尝试从缓存获取评论列表
value = cache.get(cache_key)
if value:
# 缓存命中:记录日志,直接返回缓存的评论列表
# lxy: 缓存命中:记录日志,直接返回缓存的评论列表
logger.info('get article comments:{id}'.format(id=self.id))
return value
else:
# 缓存未命中查询当前文章的已启用评论按ID降序最新评论在前
# lxy: 缓存未命中查询当前文章的已启用评论按ID降序最新评论在前
comments = self.comment_set.filter(is_enable=True).order_by('-id')
# 将评论列表存入缓存有效期100分钟60*100秒
# lxy: 将评论列表存入缓存有效期100分钟60*100秒
cache.set(cache_key, comments, 60 * 100)
# 记录日志:缓存设置成功
# lxy: 记录日志:缓存设置成功
logger.info('set article comments:{id}'.format(id=self.id))
return comments
# 生成文章在Django后台的编辑页URL用于快速跳转到后台编辑
# lxy: 生成文章在Django后台的编辑页URL用于快速跳转到后台编辑
def get_admin_url(self):
# 获取模型的元数据:(应用名,模型名)
# lxy: 获取模型的元数据:(应用名,模型名)
info = (self._meta.app_label, self._meta.model_name)
# 反向解析admin的模型修改路由传递文章主键
# lxy: 反向解析admin的模型修改路由传递文章主键
return reverse('admin:%s_%s_change' % info, args=(self.pk,))
# 缓存装饰器缓存结果100分钟
# lxy: 缓存装饰器缓存结果100分钟
@cache_decorator(expiration=60 * 100)
# 获取当前文章的下一篇文章已发布状态ID大于当前文章
# lxy: 获取当前文章的下一篇文章已发布状态ID大于当前文章
def next_article(self):
return Article.objects.filter(
id__gt=self.id, status='p').order_by('id').first()
# 缓存装饰器缓存结果100分钟
# lxy: 缓存装饰器缓存结果100分钟
@cache_decorator(expiration=60 * 100)
# 获取当前文章的前一篇文章已发布状态ID小于当前文章
# lxy: 获取当前文章的前一篇文章已发布状态ID小于当前文章
def prev_article(self):
return Article.objects.filter(id__lt=self.id, status='p').first()
# 定义分类模型Category继承自抽象基础模型BaseModel
# lxy: 定义分类模型Category继承自抽象基础模型BaseModel
class Category(BaseModel):
"""文章分类模型:存储博客文章的分类数据(支持层级分类,如父分类→子分类)"""
# 分类名称CharField类型最大长度30唯一约束避免重复分类名
# lxy: 分类名称CharField类型最大长度30唯一约束避免重复分类名
name = models.CharField(_('category name'), max_length=30, unique=True)
# 父分类:自关联外键(分类可作为其他分类的父分类),允许为空(顶级分类)
# on_delete=models.CASCADE父分类被删除时子分类也会被删除
# lxy: 父分类:自关联外键(分类可作为其他分类的父分类),允许为空(顶级分类)
# lxy: on_delete=models.CASCADE父分类被删除时子分类也会被删除
parent_category = models.ForeignKey(
'self',
verbose_name=_('parent category'),
blank=True,
null=True,
on_delete=models.CASCADE)
# 分类slugURL友好标识默认值'no-slug'用于生成分类页URL
# lxy: 分类slugURL友好标识默认值'no-slug'用于生成分类页URL
slug = models.SlugField(default='no-slug', max_length=60, blank=True)
# 排序索引IntegerField类型默认0用于控制分类显示顺序值越大越靠前
# lxy: 排序索引IntegerField类型默认0用于控制分类显示顺序值越大越靠前
index = models.IntegerField(default=0, verbose_name=_('index'))
# 模型元数据配置
# lxy: 模型元数据配置
class Meta:
ordering = ['-index'] # 默认排序:按排序索引降序
verbose_name = _('category') # 模型单数显示名称
verbose_name_plural = verbose_name # 模型复数显示名称
# 实现抽象基类的get_absolute_url方法生成分类的相对URL
# lxy: 实现抽象基类的get_absolute_url方法生成分类的相对URL
def get_absolute_url(self):
# 反向解析'blog:category_detail'路由传递分类slug作为参数
# lxy: 反向解析'blog:category_detail'路由传递分类slug作为参数
return reverse(
'blog:category_detail', kwargs={
'category_name': self.slug})
# 重写__str__方法友好显示分类名称
# lxy: 重写__str__方法友好显示分类名称
def __str__(self):
return self.name
# 缓存装饰器缓存结果10小时
# lxy: 缓存装饰器缓存结果10小时
@cache_decorator(60 * 60 * 10)
# 递归获取当前分类的所有父级分类(生成分类层级树,如子分类→父分类→顶级分类)
# lxy: 递归获取当前分类的所有父级分类(生成分类层级树,如子分类→父分类→顶级分类)
def get_category_tree(self):
"""
递归获得分类目录的父级
@ -292,7 +292,7 @@ class Category(BaseModel):
"""
categorys = []
# 内部递归函数:解析分类的父级
# lxy: 内部递归函数:解析分类的父级
def parse(category):
categorys.append(category) # 将当前分类加入列表
if category.parent_category: # 如果存在父分类,继续递归
@ -301,9 +301,9 @@ class Category(BaseModel):
parse(self) # 从当前分类开始解析
return categorys
# 缓存装饰器缓存结果10小时
# lxy: 缓存装饰器缓存结果10小时
@cache_decorator(60 * 60 * 10)
# 递归获取当前分类的所有子级分类(包括子分类的子分类)
# lxy: 递归获取当前分类的所有子级分类(包括子分类的子分类)
def get_sub_categorys(self):
"""
获得当前分类目录所有子集
@ -312,11 +312,11 @@ class Category(BaseModel):
categorys = []
all_categorys = Category.objects.all() # 获取所有分类
# 内部递归函数:解析分类的子级
# lxy: 内部递归函数:解析分类的子级
def parse(category):
if category not in categorys: # 避免重复添加(防止循环引用)
categorys.append(category)
# 查询当前分类的直接子分类
# lxy: 查询当前分类的直接子分类
childs = all_categorys.filter(parent_category=category)
for child in childs: # 遍历子分类,递归解析
if category not in categorys:
@ -327,188 +327,188 @@ class Category(BaseModel):
return categorys
# 定义标签模型Tag继承自抽象基础模型BaseModel
# lxy: 定义标签模型Tag继承自抽象基础模型BaseModel
class Tag(BaseModel):
"""文章标签模型:存储博客文章的标签数据(用于文章分类和搜索)"""
# 标签名称CharField类型最大长度30唯一约束避免重复标签名
# lxy: 标签名称CharField类型最大长度30唯一约束避免重复标签名
name = models.CharField(_('tag name'), max_length=30, unique=True)
# 标签slugURL友好标识默认值'no-slug'用于生成标签页URL
# lxy: 标签slugURL友好标识默认值'no-slug'用于生成标签页URL
slug = models.SlugField(default='no-slug', max_length=60, blank=True)
# 重写__str__方法友好显示标签名称
# lxy: 重写__str__方法友好显示标签名称
def __str__(self):
return self.name
# 实现抽象基类的get_absolute_url方法生成标签的相对URL
# lxy: 实现抽象基类的get_absolute_url方法生成标签的相对URL
def get_absolute_url(self):
# 反向解析'blog:tag_detail'路由传递标签slug作为参数
# lxy: 反向解析'blog:tag_detail'路由传递标签slug作为参数
return reverse('blog:tag_detail', kwargs={'tag_name': self.slug})
# 缓存装饰器缓存结果10小时
# lxy: 缓存装饰器缓存结果10小时
@cache_decorator(60 * 60 * 10)
# 获取当前标签关联的文章数量(去重,避免重复计数)
# lxy: 获取当前标签关联的文章数量(去重,避免重复计数)
def get_article_count(self):
return Article.objects.filter(tags__name=self.name).distinct().count()
# 模型元数据配置
# lxy: 模型元数据配置
class Meta:
ordering = ['name'] # 默认排序:按标签名称升序
verbose_name = _('tag') # 模型单数显示名称
verbose_name_plural = verbose_name # 模型复数显示名称
# 定义友情链接模型Links未继承BaseModel单独定义时间字段
# lxy: 定义友情链接模型Links未继承BaseModel单独定义时间字段
class Links(models.Model):
"""友情链接模型:存储博客的友情链接数据"""
# 链接名称CharField类型最大长度30唯一约束避免重复链接名
# lxy: 链接名称CharField类型最大长度30唯一约束避免重复链接名
name = models.CharField(_('link name'), max_length=30, unique=True)
# 链接URLURLField类型自动验证URL格式如http://、https://
# lxy: 链接URLURLField类型自动验证URL格式如http://、https://
link = models.URLField(_('link'))
# 排序序号IntegerField类型唯一约束控制友情链接显示顺序值越小越靠前
# lxy: 排序序号IntegerField类型唯一约束控制友情链接显示顺序值越小越靠前
sequence = models.IntegerField(_('order'), unique=True)
# 是否启用BooleanField类型默认True控制是否在前端显示该链接
# lxy: 是否启用BooleanField类型默认True控制是否在前端显示该链接
is_enable = models.BooleanField(
_('is show'), default=True, blank=False, null=False)
# 显示位置CharField类型长度1可选值为LinkShowType枚举默认首页显示'i'
# lxy: 显示位置CharField类型长度1可选值为LinkShowType枚举默认首页显示'i'
show_type = models.CharField(
_('show type'),
max_length=1,
choices=LinkShowType.choices,
default=LinkShowType.I)
# 创建时间DateTimeField类型默认当前时间
# lxy: 创建时间DateTimeField类型默认当前时间
creation_time = models.DateTimeField(_('creation time'), default=now)
# 最后修改时间DateTimeField类型默认当前时间
# lxy: 最后修改时间DateTimeField类型默认当前时间
last_mod_time = models.DateTimeField(_('modify time'), default=now)
# 模型元数据配置
# lxy: 模型元数据配置
class Meta:
ordering = ['sequence'] # 默认排序:按排序序号升序
verbose_name = _('link') # 模型单数显示名称
verbose_name_plural = verbose_name # 模型复数显示名称
# 重写__str__方法友好显示链接名称
# lxy: 重写__str__方法友好显示链接名称
def __str__(self):
return self.name
# 定义侧边栏模型SideBar未继承BaseModel单独定义时间字段
# lxy: 定义侧边栏模型SideBar未继承BaseModel单独定义时间字段
class SideBar(models.Model):
"""侧边栏模型存储博客侧边栏内容支持自定义HTML内容如公告、广告"""
# 侧边栏标题CharField类型最大长度100
# lxy: 侧边栏标题CharField类型最大长度100
name = models.CharField(_('title'), max_length=100)
# 侧边栏内容TextField类型支持HTML文本如公告、推荐文章列表
# lxy: 侧边栏内容TextField类型支持HTML文本如公告、推荐文章列表
content = models.TextField(_('content'))
# 排序序号IntegerField类型唯一约束控制侧边栏显示顺序值越小越靠前
# lxy: 排序序号IntegerField类型唯一约束控制侧边栏显示顺序值越小越靠前
sequence = models.IntegerField(_('order'), unique=True)
# 是否启用BooleanField类型默认True控制是否在前端显示该侧边栏
# lxy: 是否启用BooleanField类型默认True控制是否在前端显示该侧边栏
is_enable = models.BooleanField(_('is enable'), default=True)
# 创建时间DateTimeField类型默认当前时间
# lxy: 创建时间DateTimeField类型默认当前时间
creation_time = models.DateTimeField(_('creation time'), default=now)
# 最后修改时间DateTimeField类型默认当前时间
# lxy: 最后修改时间DateTimeField类型默认当前时间
last_mod_time = models.DateTimeField(_('modify time'), default=now)
# 模型元数据配置
# lxy: 模型元数据配置
class Meta:
ordering = ['sequence'] # 默认排序:按排序序号升序
verbose_name = _('sidebar') # 模型单数显示名称
verbose_name_plural = verbose_name # 模型复数显示名称
# 重写__str__方法友好显示侧边栏标题
# lxy: 重写__str__方法友好显示侧边栏标题
def __str__(self):
return self.name
# 定义博客配置模型BlogSettings
# lxy: 定义博客配置模型BlogSettings
class BlogSettings(models.Model):
"""博客全局配置模型存储博客的全局设置如站点名称、SEO信息、备案号等"""
# 站点名称CharField类型必填默认空字符串
# lxy: 站点名称CharField类型必填默认空字符串
site_name = models.CharField(
_('site name'),
max_length=200,
null=False,
blank=False,
default='')
# 站点描述TextField类型必填用于前端显示站点简介如首页底部
# lxy: 站点描述TextField类型必填用于前端显示站点简介如首页底部
site_description = models.TextField(
_('site description'),
max_length=1000,
null=False,
blank=False,
default='')
# 站点SEO描述TextField类型必填用于网页meta标签的description提升搜索引擎排名
# lxy: 站点SEO描述TextField类型必填用于网页meta标签的description提升搜索引擎排名
site_seo_description = models.TextField(
_('site seo description'), max_length=1000, null=False, blank=False, default='')
# 站点关键词TextField类型必填用于网页meta标签的keywords提升搜索引擎排名
# lxy: 站点关键词TextField类型必填用于网页meta标签的keywords提升搜索引擎排名
site_keywords = models.TextField(
_('site keywords'),
max_length=1000,
null=False,
blank=False,
default='')
# 文章摘要长度IntegerField类型默认300控制前端显示文章摘要的字符数
# lxy: 文章摘要长度IntegerField类型默认300控制前端显示文章摘要的字符数
article_sub_length = models.IntegerField(_('article sub length'), default=300)
# 侧边栏文章数量IntegerField类型默认10控制侧边栏显示的最新/热门文章数量
# lxy: 侧边栏文章数量IntegerField类型默认10控制侧边栏显示的最新/热门文章数量
sidebar_article_count = models.IntegerField(_('sidebar article count'), default=10)
# 侧边栏评论数量IntegerField类型默认5控制侧边栏显示的最新评论数量
# lxy: 侧边栏评论数量IntegerField类型默认5控制侧边栏显示的最新评论数量
sidebar_comment_count = models.IntegerField(_('sidebar comment count'), default=5)
# 文章页评论数量IntegerField类型默认5控制文章详情页默认显示的评论数量
# lxy: 文章页评论数量IntegerField类型默认5控制文章详情页默认显示的评论数量
article_comment_count = models.IntegerField(_('article comment count'), default=5)
# 是否显示谷歌广告BooleanField类型默认False控制是否在前端显示谷歌广告
# lxy: 是否显示谷歌广告BooleanField类型默认False控制是否在前端显示谷歌广告
show_google_adsense = models.BooleanField(_('show adsense'), default=False)
# 谷歌广告代码TextField类型可选存储谷歌广告的HTML代码
# lxy: 谷歌广告代码TextField类型可选存储谷歌广告的HTML代码
google_adsense_codes = models.TextField(
_('adsense code'), max_length=2000, null=True, blank=True, default='')
# 是否开放全站评论BooleanField类型默认True控制整个站点是否允许评论
# lxy: 是否开放全站评论BooleanField类型默认True控制整个站点是否允许评论
open_site_comment = models.BooleanField(_('open site comment'), default=True)
# 公共头部代码TextField类型可选存储全局头部的自定义HTML如额外CSS、JS
# lxy: 公共头部代码TextField类型可选存储全局头部的自定义HTML如额外CSS、JS
global_header = models.TextField("公共头部", null=True, blank=True, default='')
# 公共尾部代码TextField类型可选存储全局尾部的自定义HTML如备案信息、统计代码
# lxy: 公共尾部代码TextField类型可选存储全局尾部的自定义HTML如备案信息、统计代码
global_footer = models.TextField("公共尾部", null=True, blank=True, default='')
# 备案号CharField类型可选存储网站ICP备案号如"粤ICP备xxxx号"
# lxy: 备案号CharField类型可选存储网站ICP备案号如"粤ICP备xxxx号"
beian_code = models.CharField(
'备案号',
max_length=2000,
null=True,
blank=True,
default='')
# 网站统计代码TextField类型必填存储统计工具的JS代码如百度统计、谷歌分析
# lxy: 网站统计代码TextField类型必填存储统计工具的JS代码如百度统计、谷歌分析
analytics_code = models.TextField(
"网站统计代码",
max_length=1000,
null=False,
blank=False,
default='')
# 是否显示公安备案号BooleanField类型默认False控制是否显示公安备案信息
# lxy: 是否显示公安备案号BooleanField类型默认False控制是否显示公安备案信息
show_gongan_code = models.BooleanField(
'是否显示公安备案号', default=False, null=False)
# 公安备案号TextField类型可选存储公安备案号如"粤公网安备xxxx号"
# lxy: 公安备案号TextField类型可选存储公安备案号如"粤公网安备xxxx号"
gongan_beiancode = models.TextField(
'公安备案号',
max_length=2000,
null=True,
blank=True,
default='')
# 评论是否需要审核BooleanField类型默认False控制用户提交的评论是否需管理员审核后显示
# lxy: 评论是否需要审核BooleanField类型默认False控制用户提交的评论是否需管理员审核后显示
comment_need_review = models.BooleanField(
'评论是否需要审核', default=False, null=False)
# 模型元数据配置
# lxy: 模型元数据配置
class Meta:
verbose_name = _('Website configuration') # 模型单数显示名称(网站配置)
verbose_name_plural = verbose_name # 模型复数显示名称
# 重写__str__方法友好显示站点名称
# lxy: 重写__str__方法友好显示站点名称
def __str__(self):
return self.site_name
# 自定义数据验证方法:确保博客配置只能有一条记录(全局唯一配置)
# lxy: 自定义数据验证方法:确保博客配置只能有一条记录(全局唯一配置)
def clean(self):
# 排除当前实例ID后查询是否已有其他配置记录
# lxy: 排除当前实例ID后查询是否已有其他配置记录
if BlogSettings.objects.exclude(id=self.id).count():
# 若存在其他记录,抛出验证错误(阻止保存)
# lxy: 若存在其他记录,抛出验证错误(阻止保存)
raise ValidationError(_('There can only be one configuration'))
# 重写save方法保存配置后清空缓存确保前端能立即获取最新配置
# lxy: 重写save方法保存配置后清空缓存确保前端能立即获取最新配置
def save(self, *args, **kwargs):
super().save(*args, **kwargs) # 调用父类save方法完成数据入库
from djangoblog.utils import cache # 延迟导入缓存模块,避免循环导入

@ -1,28 +1,28 @@
# 从Haystack库导入索引相关核心类
# 1. SearchIndex搜索索引基类定义搜索索引的核心结构如搜索字段
# 2. Indexable索引可访问性基类要求子类实现get_model方法指定关联的Django模型
# lxy: 从Haystack库导入索引相关核心类
# lxy: 1. SearchIndex搜索索引基类定义搜索索引的核心结构如搜索字段
# lxy: 2. Indexable索引可访问性基类要求子类实现get_model方法指定关联的Django模型
from haystack import indexes
# 从当前应用blog的models.py导入Article模型用于将文章数据同步到搜索索引
# lxy: 从当前应用blog的models.py导入Article模型用于将文章数据同步到搜索索引
from blog.models import Article
# 定义文章搜索索引类ArticleIndex继承自SearchIndex搜索索引核心和Indexable索引关联模型
# 作用告诉Haystack如何构建Article模型的搜索索引指定搜索字段、关联模型及索引数据范围
# lxy: 定义文章搜索索引类ArticleIndex继承自SearchIndex搜索索引核心和Indexable索引关联模型
# lxy: 作用告诉Haystack如何构建Article模型的搜索索引指定搜索字段、关联模型及索引数据范围
class ArticleIndex(indexes.SearchIndex, indexes.Indexable):
# 定义核心搜索字段text
# - document=True标记该字段为Haystack的"文档字段"(全文搜索的核心字段,所有搜索都会基于该字段匹配)
# - use_template=True指定使用模板来构建该字段的搜索内容模板路径默认是 templates/search/indexes/blog/article_text.txt
# 模板中可包含文章标题、正文、标签等需要被搜索的字段Haystack会将这些内容整合为text字段用于搜索
# lxy: 定义核心搜索字段text
# lxy: - document=True标记该字段为Haystack的"文档字段"(全文搜索的核心字段,所有搜索都会基于该字段匹配)
# lxy: - use_template=True指定使用模板来构建该字段的搜索内容模板路径默认是 templates/search/indexes/blog/article_text.txt
# lxy: 模板中可包含文章标题、正文、标签等需要被搜索的字段Haystack会将这些内容整合为text字段用于搜索
text = indexes.CharField(document=True, use_template=True)
# 实现Indexable基类的强制方法指定当前索引关联的Django模型
# lxy: 实现Indexable基类的强制方法指定当前索引关联的Django模型
def get_model(self):
# 返回Article模型告诉Haystack该索引是为Article模型构建的
# lxy: 返回Article模型告诉Haystack该索引是为Article模型构建的
return Article
# 定义索引查询集指定哪些Article数据需要被纳入搜索索引
# lxy: 定义索引查询集指定哪些Article数据需要被纳入搜索索引
def index_queryset(self, using=None):
# using参数指定使用的搜索引擎如Elasticsearch、Whoosh默认None使用配置的默认引擎
# 过滤条件:仅将状态为"已发布"status='p')的文章纳入索引,草稿文章不参与搜索
# lxy: using参数指定使用的搜索引擎如Elasticsearch、Whoosh默认None使用配置的默认引擎
# lxy: 过滤条件:仅将状态为"已发布"status='p')的文章纳入索引,草稿文章不参与搜索
return self.get_model().objects.filter(status='p')

@ -151,8 +151,8 @@ def load_sidebar(user, linktype):
Q(show_type=str(linktype)) | Q(show_type=LinkShowType.A))
commment_list = Comment.objects.filter(is_enable=True).order_by(
'-id')[:blogsetting.sidebar_comment_count]
# 标签云 计算字体大小
# 根据总数计算出平均值 大小为 (数目/平均值)*步长
# lxy: 标签云 计算字体大小
# lxy: 根据总数计算出平均值 大小为 (数目/平均值)*步长
increment = 5
tags = Tag.objects.all()
sidebar_tags = None
@ -286,8 +286,8 @@ def load_article_detail(article, isindex, user):
}
# return only the URL of the gravatar
# TEMPLATE USE: {{ email|gravatar_url:150 }}
# lxy: return only the URL of the gravatar
# lxy: TEMPLATE USE: {{ email|gravatar_url:150 }}
@register.filter
def gravatar_url(email, size=40):
"""获得gravatar头像"""

@ -1,81 +1,81 @@
# 导入Python内置os模块用于文件路径操作如图片文件的保存与删除
# lxy: 导入Python内置os模块用于文件路径操作如图片文件的保存与删除
import os
# 导入Django项目配置模块用于获取项目设置如分页数量、文件上传路径
# lxy: 导入Django项目配置模块用于获取项目设置如分页数量、文件上传路径
from django.conf import settings
# 导入Django文件上传相关类用于模拟文件上传请求如图片上传测试
# lxy: 导入Django文件上传相关类用于模拟文件上传请求如图片上传测试
from django.core.files.uploadedfile import SimpleUploadedFile
# 导入Django管理命令调用函数用于在测试中执行自定义管理命令如重建搜索索引
# lxy: 导入Django管理命令调用函数用于在测试中执行自定义管理命令如重建搜索索引
from django.core.management import call_command
# 导入Django分页类用于测试分页功能
# lxy: 导入Django分页类用于测试分页功能
from django.core.paginator import Paginator
# 导入Django静态文件模板标签用于生成静态文件URL如测试用户头像
# lxy: 导入Django静态文件模板标签用于生成静态文件URL如测试用户头像
from django.templatetags.static import static
# 导入Django测试核心模块
# 1. Client模拟HTTP客户端用于发送GET/POST请求并获取响应
# 2. RequestFactory生成请求对象用于测试视图函数/模板标签
# 3. TestCaseDjango测试基类提供测试环境初始化、断言方法等
# lxy: 导入Django测试核心模块
# lxy: 1. Client模拟HTTP客户端用于发送GET/POST请求并获取响应
# lxy: 2. RequestFactory生成请求对象用于测试视图函数/模板标签
# lxy: 3. TestCaseDjango测试基类提供测试环境初始化、断言方法等
from django.test import Client, RequestFactory, TestCase
# 导入Django URL反向解析模块用于生成测试用的URL避免硬编码
# lxy: 导入Django URL反向解析模块用于生成测试用的URL避免硬编码
from django.urls import reverse
# 导入Django时区工具用于处理时间字段如模型创建时间
# lxy: 导入Django时区工具用于处理时间字段如模型创建时间
from django.utils import timezone
# 从accounts应用导入用户模型BlogUser自定义用户模型替代Django默认用户模型
# lxy: 从accounts应用导入用户模型BlogUser自定义用户模型替代Django默认用户模型
from accounts.models import BlogUser
# 从当前应用blog导入搜索表单BlogSearchForm用于测试表单功能
# lxy: 从当前应用blog导入搜索表单BlogSearchForm用于测试表单功能
from blog.forms import BlogSearchForm
# 从当前应用导入核心模型:文章、分类、标签、侧边栏、友情链接
# lxy: 从当前应用导入核心模型:文章、分类、标签、侧边栏、友情链接
from blog.models import Article, Category, Tag, SideBar, Links
# 从当前应用导入自定义模板标签函数,用于测试模板标签的逻辑正确性
# lxy: 从当前应用导入自定义模板标签函数,用于测试模板标签的逻辑正确性
from blog.templatetags.blog_tags import load_pagination_info, load_articletags
# 从自定义工具模块导入工具函数:
# 1. get_current_site获取当前站点信息用于生成完整URL
# 2. get_sha256生成SHA256加密字符串用于测试文件上传签名验证
# lxy: 从自定义工具模块导入工具函数:
# lxy: 1. get_current_site获取当前站点信息用于生成完整URL
# lxy: 2. get_sha256生成SHA256加密字符串用于测试文件上传签名验证
from djangoblog.utils import get_current_site, get_sha256
# 从oauth应用导入第三方登录相关模型OAuth用户、OAuth配置用于测试第三方登录数据
# lxy: 从oauth应用导入第三方登录相关模型OAuth用户、OAuth配置用于测试第三方登录数据
from oauth.models import OAuthUser, OAuthConfig
# 测试类创建标识注释Django测试框架约定测试类需继承TestCase
# Create your tests here.
# lxy: 测试类创建标识注释Django测试框架约定测试类需继承TestCase
# lxy: Create your tests here.
# 定义文章相关测试类ArticleTest继承自Django的TestCase测试基类
# 作用集中测试博客核心功能包括模型操作、URL访问、搜索、分页、文件上传等
# lxy: 定义文章相关测试类ArticleTest继承自Django的TestCase测试基类
# lxy: 作用集中测试博客核心功能包括模型操作、URL访问、搜索、分页、文件上传等
class ArticleTest(TestCase):
# 测试初始化方法:在每个测试方法执行前自动调用,用于准备测试环境
# lxy: 测试初始化方法:在每个测试方法执行前自动调用,用于准备测试环境
def setUp(self):
# 初始化HTTP客户端模拟用户发送请求
# lxy: 初始化HTTP客户端模拟用户发送请求
self.client = Client()
# 初始化请求工厂(用于生成原始请求对象,测试视图/模板标签时使用)
# lxy: 初始化请求工厂(用于生成原始请求对象,测试视图/模板标签时使用)
self.factory = RequestFactory()
# 核心测试方法测试文章相关完整流程模型创建、关联、URL访问、搜索等
# lxy: 核心测试方法测试文章相关完整流程模型创建、关联、URL访问、搜索等
def test_validate_article(self):
# 获取当前站点域名用于验证完整URL生成
# lxy: 获取当前站点域名用于验证完整URL生成
site = get_current_site().domain
# 创建/获取测试用户邮箱、用户名固定若已存在则直接获取get_or_create返回元组取第0个元素
# lxy: 创建/获取测试用户邮箱、用户名固定若已存在则直接获取get_or_create返回元组取第0个元素
user = BlogUser.objects.get_or_create(
email="liangliangyy@gmail.com",
username="liangliangyy")[0]
# 设置用户密码set_password会自动加密避免明文存储
# lxy: 设置用户密码set_password会自动加密避免明文存储
user.set_password("liangliangyy")
# 设置用户为 staff后台管理权限和 superuser超级管理员权限
# lxy: 设置用户为 staff后台管理权限和 superuser超级管理员权限
user.is_staff = True
user.is_superuser = True
# 保存用户信息到数据库
# lxy: 保存用户信息到数据库
user.save()
# 模拟访问用户个人主页验证响应状态码为200正常访问
# lxy: 模拟访问用户个人主页验证响应状态码为200正常访问
response = self.client.get(user.get_absolute_url())
self.assertEqual(response.status_code, 200)
# 模拟访问后台邮件发送日志页面测试后台URL可达性
# lxy: 模拟访问后台邮件发送日志页面测试后台URL可达性
response = self.client.get('/admin/servermanager/emailsendlog/')
# 模拟访问后台操作日志列表页面测试后台URL可达性
# lxy: 模拟访问后台操作日志列表页面测试后台URL可达性
response = self.client.get('admin/admin/logentry/')
# 创建测试侧边栏数据
# lxy: 创建测试侧边栏数据
s = SideBar()
s.sequence = 1 # 排序序号(控制显示顺序)
s.name = 'test' # 侧边栏标题
@ -83,19 +83,19 @@ class ArticleTest(TestCase):
s.is_enable = True # 启用侧边栏
s.save() # 保存到数据库
# 创建测试分类数据
# lxy: 创建测试分类数据
category = Category()
category.name = "category" # 分类名称
category.creation_time = timezone.now() # 创建时间
category.last_mod_time = timezone.now() # 最后修改时间
category.save() # 保存到数据库
# 创建测试标签数据
# lxy: 创建测试标签数据
tag = Tag()
tag.name = "nicetag" # 标签名称
tag.save() # 保存到数据库
# 创建测试文章数据
# lxy: 创建测试文章数据
article = Article()
article.title = "nicetitle" # 文章标题
article.body = "nicecontent" # 文章内容
@ -105,15 +105,15 @@ class ArticleTest(TestCase):
article.status = 'p' # 文章状态:已发布('p'=Published
article.save() # 保存到数据库
# 验证文章初始标签数量为0未添加标签
# lxy: 验证文章初始标签数量为0未添加标签
self.assertEqual(0, article.tags.count())
# 为文章添加标签(多对多关联)
# lxy: 为文章添加标签(多对多关联)
article.tags.add(tag)
article.save() # 再次保存,更新关联关系
# 验证文章标签数量为1添加成功
# lxy: 验证文章标签数量为1添加成功
self.assertEqual(1, article.tags.count())
# 批量创建20篇测试文章用于测试分页功能
# lxy: 批量创建20篇测试文章用于测试分页功能
for i in range(20):
article = Article()
article.title = "nicetitle" + str(i) # 标题带序号,避免重复
@ -126,169 +126,169 @@ class ArticleTest(TestCase):
article.tags.add(tag) # 关联同一标签
article.save() # 更新关联关系
# 导入Elasticsearch启用标识判断是否启用搜索引擎
# lxy: 导入Elasticsearch启用标识判断是否启用搜索引擎
from blog.documents import ELASTICSEARCH_ENABLED
# 若启用Elasticsearch测试搜索功能
# lxy: 若启用Elasticsearch测试搜索功能
if ELASTICSEARCH_ENABLED:
call_command("build_index") # 执行自定义管理命令,重建搜索索引
# 模拟发送搜索请求,关键词为'nicetitle'
# lxy: 模拟发送搜索请求,关键词为'nicetitle'
response = self.client.get('/search', {'q': 'nicetitle'})
self.assertEqual(response.status_code, 200) # 验证搜索页面正常响应
# 模拟访问最后一篇测试文章的详情页验证响应状态码为200
# lxy: 模拟访问最后一篇测试文章的详情页验证响应状态码为200
response = self.client.get(article.get_absolute_url())
self.assertEqual(response.status_code, 200)
# 导入爬虫通知工具类,测试文章发布后通知搜索引擎(如百度)
# lxy: 导入爬虫通知工具类,测试文章发布后通知搜索引擎(如百度)
from djangoblog.spider_notify import SpiderNotify
SpiderNotify.notify(article.get_absolute_url()) # 通知搜索引擎该文章URL
# 模拟访问标签详情页验证响应状态码为200
# lxy: 模拟访问标签详情页验证响应状态码为200
response = self.client.get(tag.get_absolute_url())
self.assertEqual(response.status_code, 200)
# 模拟访问分类详情页验证响应状态码为200
# lxy: 模拟访问分类详情页验证响应状态码为200
response = self.client.get(category.get_absolute_url())
self.assertEqual(response.status_code, 200)
# 模拟搜索不存在的关键词'django'验证搜索页面正常响应状态码200
# lxy: 模拟搜索不存在的关键词'django'验证搜索页面正常响应状态码200
response = self.client.get('/search', {'q': 'django'})
self.assertEqual(response.status_code, 200)
# 测试自定义模板标签load_articletags验证返回结果不为空
# lxy: 测试自定义模板标签load_articletags验证返回结果不为空
s = load_articletags(article)
self.assertIsNotNone(s)
# 模拟用户登录(使用测试用户的用户名和密码)
# lxy: 模拟用户登录(使用测试用户的用户名和密码)
self.client.login(username='liangliangyy', password='liangliangyy')
# 模拟访问文章归档页面验证响应状态码为200
# lxy: 模拟访问文章归档页面验证响应状态码为200
response = self.client.get(reverse('blog:archives'))
self.assertEqual(response.status_code, 200)
# 测试所有文章的分页功能使用项目配置的分页数量settings.PAGINATE_BY
# lxy: 测试所有文章的分页功能使用项目配置的分页数量settings.PAGINATE_BY
p = Paginator(Article.objects.all(), settings.PAGINATE_BY)
self.check_pagination(p, '', '') # 调用自定义分页测试方法
# 测试标签归档的分页功能:筛选指定标签的文章
# lxy: 测试标签归档的分页功能:筛选指定标签的文章
p = Paginator(Article.objects.filter(tags=tag), settings.PAGINATE_BY)
self.check_pagination(p, '分类标签归档', tag.slug) # 传递分页类型和标签slug
# 测试作者归档的分页功能:筛选指定作者的文章
# lxy: 测试作者归档的分页功能:筛选指定作者的文章
p = Paginator(
Article.objects.filter(
author__username='liangliangyy'), settings.PAGINATE_BY)
self.check_pagination(p, '作者文章归档', 'liangliangyy') # 传递分页类型和作者名
# 测试分类归档的分页功能:筛选指定分类的文章
# lxy: 测试分类归档的分页功能:筛选指定分类的文章
p = Paginator(Article.objects.filter(category=category), settings.PAGINATE_BY)
self.check_pagination(p, '分类目录归档', category.slug) # 传递分页类型和分类slug
# 测试搜索表单初始化表单并调用search方法验证表单逻辑无报错
# lxy: 测试搜索表单初始化表单并调用search方法验证表单逻辑无报错
f = BlogSearchForm()
f.search()
# 测试百度爬虫通知功能通知单篇文章的完整URL
# lxy: 测试百度爬虫通知功能通知单篇文章的完整URL
from djangoblog.spider_notify import SpiderNotify
SpiderNotify.baidu_notify([article.get_full_url()])
# 测试头像相关模板标签验证gravatar_url和gravatar函数正常返回结果
# lxy: 测试头像相关模板标签验证gravatar_url和gravatar函数正常返回结果
from blog.templatetags.blog_tags import gravatar_url, gravatar
u = gravatar_url('liangliangyy@gmail.com') # 生成Gravatar头像URL
u = gravatar('liangliangyy@gmail.com') # 生成Gravatar头像HTML标签
# 创建测试友情链接数据
# lxy: 创建测试友情链接数据
link = Links(
sequence=1, # 排序序号
name="lylinux", # 链接名称
link='https://wwww.lylinux.net') # 链接URL
link.save() # 保存到数据库
# 模拟访问友情链接页面验证响应状态码为200
# lxy: 模拟访问友情链接页面验证响应状态码为200
response = self.client.get('/links.html')
self.assertEqual(response.status_code, 200)
# 模拟访问RSS订阅 feed 页面验证响应状态码为200
# lxy: 模拟访问RSS订阅 feed 页面验证响应状态码为200
response = self.client.get('/feed/')
self.assertEqual(response.status_code, 200)
# 模拟访问站点地图页面验证响应状态码为200
# lxy: 模拟访问站点地图页面验证响应状态码为200
response = self.client.get('/sitemap.xml')
self.assertEqual(response.status_code, 200)
# 模拟访问后台文章删除页面(测试后台操作可达性)
# lxy: 模拟访问后台文章删除页面(测试后台操作可达性)
self.client.get("/admin/blog/article/1/delete/")
# 模拟访问后台邮件发送日志页面(重复访问,验证稳定性)
# lxy: 模拟访问后台邮件发送日志页面(重复访问,验证稳定性)
self.client.get('/admin/servermanager/emailsendlog/')
# 模拟访问后台操作日志列表页面(重复访问,验证稳定性)
# lxy: 模拟访问后台操作日志列表页面(重复访问,验证稳定性)
self.client.get('/admin/admin/logentry/')
# 模拟访问后台操作日志编辑页面(测试后台详情页可达性)
# lxy: 模拟访问后台操作日志编辑页面(测试后台详情页可达性)
self.client.get('/admin/admin/logentry/1/change/')
# 自定义分页测试方法验证分页逻辑和分页URL的可达性
# lxy: 自定义分页测试方法验证分页逻辑和分页URL的可达性
def check_pagination(self, p, type, value):
# 遍历所有分页页面从第1页到最后一页
# lxy: 遍历所有分页页面从第1页到最后一页
for page in range(1, p.num_pages + 1):
# 调用模板标签load_pagination_info获取分页信息上一页URL、下一页URL等
# lxy: 调用模板标签load_pagination_info获取分页信息上一页URL、下一页URL等
s = load_pagination_info(p.page(page), type, value)
self.assertIsNotNone(s) # 验证分页信息不为空
# 若存在上一页URL模拟访问并验证响应状态码为200
# lxy: 若存在上一页URL模拟访问并验证响应状态码为200
if s['previous_url']:
response = self.client.get(s['previous_url'])
self.assertEqual(response.status_code, 200)
# 若存在下一页URL模拟访问并验证响应状态码为200
# lxy: 若存在下一页URL模拟访问并验证响应状态码为200
if s['next_url']:
response = self.client.get(s['next_url'])
self.assertEqual(response.status_code, 200)
# 测试方法:测试图片上传功能(含未授权上传、授权上传验证)
# lxy: 测试方法:测试图片上传功能(含未授权上传、授权上传验证)
def test_image(self):
# 导入requests库需提前安装用于下载测试图片
# lxy: 导入requests库需提前安装用于下载测试图片
import requests
# 下载Python官网logo图片作为测试上传文件
# lxy: 下载Python官网logo图片作为测试上传文件
rsp = requests.get(
'https://www.python.org/static/img/python-logo@2x.png')
# 定义图片保存路径:项目根目录下的'python.png'
# lxy: 定义图片保存路径:项目根目录下的'python.png'
imagepath = os.path.join(settings.BASE_DIR, 'python.png')
# 将下载的图片内容写入本地文件
# lxy: 将下载的图片内容写入本地文件
with open(imagepath, 'wb') as file:
file.write(rsp.content)
# 模拟未授权的图片上传请求未带签名验证响应状态码为403禁止访问
# lxy: 模拟未授权的图片上传请求未带签名验证响应状态码为403禁止访问
rsp = self.client.post('/upload')
self.assertEqual(rsp.status_code, 403)
# 生成上传授权签名双重SHA256加密项目SECRET_KEY与后端上传接口的签名验证逻辑一致
# lxy: 生成上传授权签名双重SHA256加密项目SECRET_KEY与后端上传接口的签名验证逻辑一致
sign = get_sha256(get_sha256(settings.SECRET_KEY))
# 读取本地测试图片文件创建SimpleUploadedFile对象模拟文件上传数据
# lxy: 读取本地测试图片文件创建SimpleUploadedFile对象模拟文件上传数据
with open(imagepath, 'rb') as file:
imgfile = SimpleUploadedFile(
'python.png', file.read(), content_type='image/jpg') # 文件名、文件内容、MIME类型
form_data = {'python.png': imgfile} # 构造表单数据(键为文件名,值为文件对象)
# 模拟带签名的图片上传请求跟随重定向follow=True
# lxy: 模拟带签名的图片上传请求跟随重定向follow=True
rsp = self.client.post(
'/upload?sign=' + sign, form_data, follow=True)
self.assertEqual(rsp.status_code, 200) # 验证授权上传成功响应状态码为200
# 删除本地测试图片文件(清理测试残留)
# lxy: 删除本地测试图片文件(清理测试残留)
os.remove(imagepath)
# 测试工具函数:保存用户头像、发送邮件
# lxy: 测试工具函数:保存用户头像、发送邮件
from djangoblog.utils import save_user_avatar, send_email
send_email(['qq@qq.com'], 'testTitle', 'testContent') # 发送测试邮件(收件人、标题、内容)
# 从URL保存用户头像测试图片下载与保存逻辑
# lxy: 从URL保存用户头像测试图片下载与保存逻辑
save_user_avatar(
'https://www.python.org/static/img/python-logo@2x.png')
# 测试方法测试错误页面404页面
# lxy: 测试方法测试错误页面404页面
def test_errorpage(self):
# 模拟访问不存在的URL'/eee'验证响应状态码为404页面未找到
# lxy: 模拟访问不存在的URL'/eee'验证响应状态码为404页面未找到
rsp = self.client.get('/eee')
self.assertEqual(rsp.status_code, 404)
# 测试方法:测试自定义管理命令(如重建索引、清理缓存等)
# lxy: 测试方法:测试自定义管理命令(如重建索引、清理缓存等)
def test_commands(self):
# 创建/获取测试用户与test_validate_article方法一致
# lxy: 创建/获取测试用户与test_validate_article方法一致
user = BlogUser.objects.get_or_create(
email="liangliangyy@gmail.com",
username="liangliangyy")[0]
@ -297,14 +297,14 @@ class ArticleTest(TestCase):
user.is_superuser = True
user.save()
# 创建第三方登录配置QQ登录
# lxy: 创建第三方登录配置QQ登录
c = OAuthConfig()
c.type = 'qq' # 登录类型QQ
c.appkey = 'appkey' # 测试用appkey
c.appsecret = 'appsecret' # 测试用appsecret
c.save() # 保存到数据库
# 创建第三方登录用户(关联测试用户)
# lxy: 创建第三方登录用户(关联测试用户)
u = OAuthUser()
u.type = 'qq' # 登录类型QQ
u.openid = 'openid' # 测试用openid第三方平台用户唯一标识
@ -316,11 +316,11 @@ class ArticleTest(TestCase):
}''' # 第三方用户元数据如QQ头像URL
u.save() # 保存到数据库
# 创建另一个第三方登录用户不关联本地用户测试头像URL直接赋值
# lxy: 创建另一个第三方登录用户不关联本地用户测试头像URL直接赋值
u = OAuthUser()
u.type = 'qq' # 登录类型QQ
u.openid = 'openid1' # 不同的openid
# 直接赋值头像URL而非静态文件
# lxy: 直接赋值头像URL而非静态文件
u.picture = 'https://qzapp.qlogo.cn/qzapp/101513904/C740E30B4113EAA80E0D9918ABC78E82/30'
u.metadata = '''
{
@ -328,19 +328,19 @@ class ArticleTest(TestCase):
}''' # 第三方用户元数据
u.save() # 保存到数据库
# 导入Elasticsearch启用标识
# lxy: 导入Elasticsearch启用标识
from blog.documents import ELASTICSEARCH_ENABLED
# 若启用Elasticsearch执行重建搜索索引命令
# lxy: 若启用Elasticsearch执行重建搜索索引命令
if ELASTICSEARCH_ENABLED:
call_command("build_index")
# 执行自定义管理命令:通知百度爬虫(参数"all"表示通知所有文章)
# lxy: 执行自定义管理命令:通知百度爬虫(参数"all"表示通知所有文章)
call_command("ping_baidu", "all")
# 执行自定义管理命令:创建测试数据
# lxy: 执行自定义管理命令:创建测试数据
call_command("create_testdata")
# 执行自定义管理命令:清理缓存
# lxy: 执行自定义管理命令:清理缓存
call_command("clear_cache")
# 执行自定义管理命令:同步用户头像(从第三方平台同步头像到本地)
# lxy: 执行自定义管理命令:同步用户头像(从第三方平台同步头像到本地)
call_command("sync_user_avatar")
# 执行自定义管理命令:构建搜索关键词(优化搜索体验)
# lxy: 执行自定义管理命令:构建搜索关键词(优化搜索体验)
call_command("build_search_words")

@ -1,90 +1,90 @@
# 导入Django URL路由核心模块用于定义路由规则
# lxy: 导入Django URL路由核心模块用于定义路由规则
from django.urls import path
# 导入Django缓存装饰器用于为视图添加页面缓存提升访问性能
# lxy: 导入Django缓存装饰器用于为视图添加页面缓存提升访问性能
from django.views.decorators.cache import cache_page
# 从当前应用blog导入视图模块路由将映射到对应的视图类/函数
# lxy: 从当前应用blog导入视图模块路由将映射到对应的视图类/函数
from . import views
# 定义应用命名空间app_name用于模板URL反向解析时区分不同应用的路由
# 示例:模板中使用 {% url 'blog:index' %} 反向生成首页URL
# lxy: 定义应用命名空间app_name用于模板URL反向解析时区分不同应用的路由
# lxy: 示例:模板中使用 {% url 'blog:index' %} 反向生成首页URL
app_name = "blog"
# 定义URL路由列表每个path对应一个路由规则按匹配优先级排序
# lxy: 定义URL路由列表每个path对应一个路由规则按匹配优先级排序
urlpatterns = [
# 1. 首页路由:匹配根路径(空字符串)
# lxy: 1. 首页路由:匹配根路径(空字符串)
path(
r'', # 路由路径:根路径(网站首页)
views.IndexView.as_view(), # 关联视图IndexView类视图的as_view()方法(类视图转为可调用视图)
name='index'), # 路由名称:用于反向解析,名称为'index'
# 2. 分页首页路由:匹配带页码的首页(如/page/2/
# lxy: 2. 分页首页路由:匹配带页码的首页(如/page/2/
path(
r'page/<int:page>/', # 路由路径:<int:page>为路径参数int指定参数类型为整数page为参数名
views.IndexView.as_view(), # 复用首页视图类视图会通过page参数处理分页逻辑
name='index_page'), # 路由名称:用于反向解析分页首页,名称为'index_page'
# 3. 文章详情页路由按ID匹配匹配带发布日期和文章ID的URL如/article/2025/11/5/10.html
# lxy: 3. 文章详情页路由按ID匹配匹配带发布日期和文章ID的URL如/article/2025/11/5/10.html
path(
r'article/<int:year>/<int:month>/<int:day>/<int:article_id>.html', # 路径参数年、月、日整数、文章ID整数
views.ArticleDetailView.as_view(), # 关联文章详情视图类
name='detailbyid'), # 路由名称:用于反向解析文章详情页,名称为'detailbyid'
# 4. 分类详情页路由匹配分类slug对应的页面如/category/python.html
# lxy: 4. 分类详情页路由匹配分类slug对应的页面如/category/python.html
path(
r'category/<slug:category_name>.html', # 路径参数:<slug:category_name>slug类型支持字母、数字、连字符、下划线
views.CategoryDetailView.as_view(), # 关联分类详情视图类
name='category_detail'), # 路由名称:用于反向解析分类首页,名称为'category_detail'
# 5. 分类分页路由:匹配带页码的分类页(如/category/python/3.html
# lxy: 5. 分类分页路由:匹配带页码的分类页(如/category/python/3.html
path(
r'category/<slug:category_name>/<int:page>.html', # 路径参数分类slug + 分页页码(整数)
views.CategoryDetailView.as_view(), # 复用分类详情视图类,处理分页逻辑
name='category_detail_page'), # 路由名称:用于反向解析分类分页,名称为'category_detail_page'
# 6. 作者文章列表路由:匹配作者名称对应的文章列表页(如/author/liangliangyy.html
# lxy: 6. 作者文章列表路由:匹配作者名称对应的文章列表页(如/author/liangliangyy.html
path(
r'author/<author_name>.html', # 路径参数author_name字符串类型默认不限制字符匹配作者用户名
views.AuthorDetailView.as_view(), # 关联作者文章列表视图类
name='author_detail'), # 路由名称:用于反向解析作者首页,名称为'author_detail'
# 7. 作者文章分页路由:匹配带页码的作者文章列表页(如/author/liangliangyy/2.html
# lxy: 7. 作者文章分页路由:匹配带页码的作者文章列表页(如/author/liangliangyy/2.html
path(
r'author/<author_name>/<int:page>.html', # 路径参数:作者名称 + 分页页码(整数)
views.AuthorDetailView.as_view(), # 复用作者文章列表视图类,处理分页逻辑
name='author_detail_page'), # 路由名称:用于反向解析作者分页,名称为'author_detail_page'
# 8. 标签详情页路由匹配标签slug对应的文章列表页如/tag/django.html
# lxy: 8. 标签详情页路由匹配标签slug对应的文章列表页如/tag/django.html
path(
r'tag/<slug:tag_name>.html', # 路径参数:<slug:tag_name>标签的URL友好标识
views.TagDetailView.as_view(), # 关联标签详情视图类
name='tag_detail'), # 路由名称:用于反向解析标签首页,名称为'tag_detail'
# 9. 标签分页路由:匹配带页码的标签文章列表页(如/tag/django/4.html
# lxy: 9. 标签分页路由:匹配带页码的标签文章列表页(如/tag/django/4.html
path(
r'tag/<slug:tag_name>/<int:page>.html', # 路径参数标签slug + 分页页码(整数)
views.TagDetailView.as_view(), # 复用标签详情视图类,处理分页逻辑
name='tag_detail_page'), # 路由名称:用于反向解析标签分页,名称为'tag_detail_page'
# 10. 文章归档页路由匹配archives.html路径如/archives.html
# lxy: 10. 文章归档页路由匹配archives.html路径如/archives.html
path(
'archives.html', # 路由路径:固定为'archives.html'
cache_page(60 * 60)(views.ArchivesView.as_view()), # 添加页面缓存缓存60*60=3600秒1小时提升访问性能
name='archives'), # 路由名称:用于反向解析归档页,名称为'archives'
# 11. 友情链接页路由匹配links.html路径如/links.html
# lxy: 11. 友情链接页路由匹配links.html路径如/links.html
path(
'links.html', # 路由路径:固定为'links.html'
views.LinkListView.as_view(), # 关联友情链接列表视图类
name='links'), # 路由名称:用于反向解析友情链接页,名称为'links'
# 12. 文件上传接口路由:匹配'upload'路径(如/upload
# lxy: 12. 文件上传接口路由:匹配'upload'路径(如/upload
path(
r'upload', # 路由路径:固定为'upload'(无后缀)
views.fileupload, # 关联文件上传视图函数(非类视图,直接关联函数)
name='upload'), # 路由名称:用于反向解析上传接口,名称为'upload'
# 13. 清理缓存接口路由:匹配'clean'路径(如/clean
# lxy: 13. 清理缓存接口路由:匹配'clean'路径(如/clean
path(
r'clean', # 路由路径:固定为'clean'(无后缀)
views.clean_cache_view, # 关联清理缓存视图函数

@ -1,99 +1,99 @@
# 导入Python内置logging模块用于记录视图运行日志如缓存命中、错误信息
# lxy: 导入Python内置logging模块用于记录视图运行日志如缓存命中、错误信息
import logging
# 导入Python内置os模块用于文件路径操作如文件上传时创建目录
# lxy: 导入Python内置os模块用于文件路径操作如文件上传时创建目录
import os
# 导入Python内置uuid模块用于生成唯一文件名避免文件上传时重名
# lxy: 导入Python内置uuid模块用于生成唯一文件名避免文件上传时重名
import uuid
# 导入Django项目配置模块用于获取项目设置如分页数量、文件上传路径
# lxy: 导入Django项目配置模块用于获取项目设置如分页数量、文件上传路径
from django.conf import settings
# 导入Django分页类用于处理评论分页逻辑
# lxy: 导入Django分页类用于处理评论分页逻辑
from django.core.paginator import Paginator
# 导入DjangoHTTP响应类
# 1. HttpResponse返回普通响应
# 2. HttpResponseForbidden返回403禁止访问响应
# lxy: 导入DjangoHTTP响应类
# lxy: 1. HttpResponse返回普通响应
# lxy: 2. HttpResponseForbidden返回403禁止访问响应
from django.http import HttpResponse, HttpResponseForbidden
# 导入Django快捷函数
# 1. get_object_or_404获取对象不存在则返回404页面
# 2. render渲染模板并返回响应
# lxy: 导入Django快捷函数
# lxy: 1. get_object_or_404获取对象不存在则返回404页面
# lxy: 2. render渲染模板并返回响应
from django.shortcuts import get_object_or_404
from django.shortcuts import render
# 导入Django静态文件模板标签用于生成静态文件URL如上传文件的访问URL
# lxy: 导入Django静态文件模板标签用于生成静态文件URL如上传文件的访问URL
from django.templatetags.static import static
# 导入Django时区工具用于处理时间相关操作如文件上传目录按日期划分
# lxy: 导入Django时区工具用于处理时间相关操作如文件上传目录按日期划分
from django.utils import timezone
# 导入Django国际化翻译工具用于错误信息的多语言支持
# lxy: 导入Django国际化翻译工具用于错误信息的多语言支持
from django.utils.translation import gettext_lazy as _
# 导入Django CSRF豁免装饰器用于文件上传接口避免CSRF验证
# lxy: 导入Django CSRF豁免装饰器用于文件上传接口避免CSRF验证
from django.views.decorators.csrf import csrf_exempt
# 导入Django通用视图
# 1. DetailView详情页通用视图适用于单条数据展示如文章详情
# 2. ListView列表页通用视图适用于多条数据展示如文章列表、分类列表
# lxy: 导入Django通用视图
# lxy: 1. DetailView详情页通用视图适用于单条数据展示如文章详情
# lxy: 2. ListView列表页通用视图适用于多条数据展示如文章列表、分类列表
from django.views.generic.detail import DetailView
from django.views.generic.list import ListView
# 导入Haystack搜索视图用于实现全文搜索功能
# lxy: 导入Haystack搜索视图用于实现全文搜索功能
from haystack.views import SearchView
# 从当前应用导入核心模型:文章、分类、链接显示类型、友情链接、标签
# lxy: 从当前应用导入核心模型:文章、分类、链接显示类型、友情链接、标签
from blog.models import Article, Category, LinkShowType, Links, Tag
# 从comments应用导入评论表单用于文章详情页的评论提交
# lxy: 从comments应用导入评论表单用于文章详情页的评论提交
from comments.forms import CommentForm
# 从自定义工具模块导入工具函数:
# 1. cache缓存操作对象用于读写缓存
# 2. get_blog_setting获取博客全局配置如评论分页数量
# 3. get_sha256生成SHA256加密字符串用于文件上传签名验证
# lxy: 从自定义工具模块导入工具函数:
# lxy: 1. cache缓存操作对象用于读写缓存
# lxy: 2. get_blog_setting获取博客全局配置如评论分页数量
# lxy: 3. get_sha256生成SHA256加密字符串用于文件上传签名验证
from djangoblog.utils import cache, get_blog_setting, get_sha256
# 创建日志记录器日志名称与当前模块一致__name__便于定位日志来源
# lxy: 创建日志记录器日志名称与当前模块一致__name__便于定位日志来源
logger = logging.getLogger(__name__)
# 定义文章列表基础视图类ArticleListView继承自Django的ListView列表页通用视图
# 作用:封装所有列表类视图(首页、分类列表、标签列表、作者列表)的公共逻辑(分页、缓存、上下文处理)
# lxy: 定义文章列表基础视图类ArticleListView继承自Django的ListView列表页通用视图
# lxy: 作用:封装所有列表类视图(首页、分类列表、标签列表、作者列表)的公共逻辑(分页、缓存、上下文处理)
class ArticleListView(ListView):
# template_name指定列表页渲染模板所有子类可复用或重写
# lxy: template_name指定列表页渲染模板所有子类可复用或重写
template_name = 'blog/article_index.html'
# context_object_name指定模板中使用的上下文变量名模板中通过{{ article_list }}访问列表数据)
# lxy: context_object_name指定模板中使用的上下文变量名模板中通过{{ article_list }}访问列表数据)
context_object_name = 'article_list'
# page_type页面类型标识用于模板显示标题如"分类目录归档"),子类需重写
# lxy: page_type页面类型标识用于模板显示标题如"分类目录归档"),子类需重写
page_type = ''
# paginate_by分页数量从项目配置中读取settings.PAGINATE_BY
# lxy: paginate_by分页数量从项目配置中读取settings.PAGINATE_BY
paginate_by = settings.PAGINATE_BY
# page_kwarg分页参数名URL中用于传递页码的参数默认'page'
# lxy: page_kwarg分页参数名URL中用于传递页码的参数默认'page'
page_kwarg = 'page'
# link_type友情链接显示类型关联LinkShowType枚举控制不同页面显示不同链接
# lxy: link_type友情链接显示类型关联LinkShowType枚举控制不同页面显示不同链接
link_type = LinkShowType.L
# 定义获取视图缓存key的方法当前未实际使用预留扩展
# lxy: 定义获取视图缓存key的方法当前未实际使用预留扩展
def get_view_cache_key(self):
return self.request.get['pages']
# 页码属性:通过@property装饰器将方法转为属性统一获取当前页码从URL参数或默认值1
# lxy: 页码属性:通过@property装饰器将方法转为属性统一获取当前页码从URL参数或默认值1
@property
def page_number(self):
page_kwarg = self.page_kwarg
# 从URL路径参数、GET参数中获取页码均无则默认1
# lxy: 从URL路径参数、GET参数中获取页码均无则默认1
page = self.kwargs.get(
page_kwarg) or self.request.GET.get(page_kwarg) or 1
return page
# 定义获取查询集缓存key的抽象方法子类必须重写确保缓存key唯一
# lxy: 定义获取查询集缓存key的抽象方法子类必须重写确保缓存key唯一
def get_queryset_cache_key(self):
"""
子类重写.获得queryset的缓存key
"""
raise NotImplementedError()
# 定义获取查询集数据的抽象方法(子类必须重写,实现具体数据查询逻辑)
# lxy: 定义获取查询集数据的抽象方法(子类必须重写,实现具体数据查询逻辑)
def get_queryset_data(self):
"""
子类重写.获取queryset的数据
"""
raise NotImplementedError()
# 从缓存中获取查询集数据(缓存命中则直接返回,未命中则查询并缓存)
# lxy: 从缓存中获取查询集数据(缓存命中则直接返回,未命中则查询并缓存)
def get_queryset_from_cache(self, cache_key):
'''
缓存页面数据
@ -102,17 +102,17 @@ class ArticleListView(ListView):
'''
value = cache.get(cache_key) # 尝试从缓存获取数据
if value:
# 缓存命中:记录日志并返回数据
# lxy: 缓存命中:记录日志并返回数据
logger.info('get view cache.key:{key}'.format(key=cache_key))
return value
else:
# 缓存未命中调用子类实现的get_queryset_data获取数据库数据
# lxy: 缓存未命中调用子类实现的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
# 重写ListView的get_queryset方法从缓存获取数据替代默认直接查询数据库
# lxy: 重写ListView的get_queryset方法从缓存获取数据替代默认直接查询数据库
def get_queryset(self):
'''
重写默认从缓存获取数据
@ -122,34 +122,34 @@ class ArticleListView(ListView):
value = self.get_queryset_from_cache(key) # 从缓存获取数据
return value
# 重写ListView的get_context_data方法添加额外上下文变量友情链接显示类型
# lxy: 重写ListView的get_context_data方法添加额外上下文变量友情链接显示类型
def get_context_data(self, **kwargs):
kwargs['linktype'] = self.link_type # 传递友情链接显示类型到模板
# 调用父类方法,保留原有上下文数据(如分页数据、文章列表)
# lxy: 调用父类方法,保留原有上下文数据(如分页数据、文章列表)
return super(ArticleListView, self).get_context_data(** kwargs)
# 定义首页视图类IndexView继承自ArticleListView复用列表页公共逻辑
# lxy: 定义首页视图类IndexView继承自ArticleListView复用列表页公共逻辑
class IndexView(ArticleListView):
'''
首页视图展示已发布的普通文章列表
'''
# 重写友情链接显示类型首页显示LinkShowType.I
# lxy: 重写友情链接显示类型首页显示LinkShowType.I
link_type = LinkShowType.I
# 实现父类抽象方法:获取首页文章数据(查询已发布的普通文章)
# lxy: 实现父类抽象方法:获取首页文章数据(查询已发布的普通文章)
def get_queryset_data(self):
# 筛选条件type='a'普通文章、status='p'(已发布)
# lxy: 筛选条件type='a'普通文章、status='p'(已发布)
article_list = Article.objects.filter(type='a', status='p')
return article_list
# 实现父类抽象方法生成首页缓存key包含页码确保不同分页缓存独立
# lxy: 实现父类抽象方法生成首页缓存key包含页码确保不同分页缓存独立
def get_queryset_cache_key(self):
cache_key = 'index_{page}'.format(page=self.page_number)
return cache_key
# 定义文章详情页视图类ArticleDetailView继承自Django的DetailView详情页通用视图
# lxy: 定义文章详情页视图类ArticleDetailView继承自Django的DetailView详情页通用视图
class ArticleDetailView(DetailView):
'''
文章详情页面视图展示单篇文章详情评论列表及评论分页
@ -159,28 +159,28 @@ class ArticleDetailView(DetailView):
pk_url_kwarg = 'article_id' # URL中传递文章ID的参数名与路由配置一致
context_object_name = "article" # 模板中使用的上下文变量名({{ article }}访问文章数据)
# 重写DetailView的get_object方法获取文章对象后更新浏览量
# lxy: 重写DetailView的get_object方法获取文章对象后更新浏览量
def get_object(self, queryset=None):
# 调用父类方法获取文章对象
# lxy: 调用父类方法获取文章对象
obj = super(ArticleDetailView, self).get_object()
obj.viewed() # 调用Article模型的viewed方法浏览量+1
self.object = obj # 保存文章对象到实例属性
return obj
# 重写DetailView的get_context_data方法添加评论表单、评论列表、分页等额外上下文
# lxy: 重写DetailView的get_context_data方法添加评论表单、评论列表、分页等额外上下文
def get_context_data(self, **kwargs):
comment_form = CommentForm() # 初始化评论表单(供用户提交评论)
# 获取当前文章的评论列表从缓存或数据库Article模型的comment_list方法已实现缓存
# lxy: 获取当前文章的评论列表从缓存或数据库Article模型的comment_list方法已实现缓存
article_comments = self.object.comment_list()
# 筛选顶级评论parent_comment=None无父评论的评论
# lxy: 筛选顶级评论parent_comment=None无父评论的评论
parent_comments = article_comments.filter(parent_comment=None)
# 获取博客全局配置(如文章页评论分页数量)
# lxy: 获取博客全局配置(如文章页评论分页数量)
blog_setting = get_blog_setting()
# 初始化评论分页器(按配置的评论数量分页)
# lxy: 初始化评论分页器(按配置的评论数量分页)
paginator = Paginator(parent_comments, blog_setting.article_comment_count)
# 从GET参数获取评论页码默认1若参数非法则重置为1
# lxy: 从GET参数获取评论页码默认1若参数非法则重置为1
page = self.request.GET.get('comment_page', '1')
if not page.isnumeric():
page = 1
@ -191,61 +191,61 @@ class ArticleDetailView(DetailView):
if page > paginator.num_pages:
page = paginator.num_pages
# 获取当前页的评论数据
# lxy: 获取当前页的评论数据
p_comments = paginator.page(page)
# 计算下一页、上一页页码无则为None
# lxy: 计算下一页、上一页页码无则为None
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
# 生成下一页评论URL含锚点#commentlist-container直接跳转到评论区
# lxy: 生成下一页评论URL含锚点#commentlist-container直接跳转到评论区
if next_page:
kwargs[
'comment_next_page_url'] = self.object.get_absolute_url() + f'?comment_page={next_page}#commentlist-container'
# 生成上一页评论URL
# lxy: 生成上一页评论URL
if prev_page:
kwargs[
'comment_prev_page_url'] = self.object.get_absolute_url() + f'?comment_page={prev_page}#commentlist-container'
# 向上下文添加额外数据(供模板使用)
# lxy: 向上下文添加额外数据(供模板使用)
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 # 评论总数
# 上一篇、下一篇文章从缓存获取Article模型的next_article/prev_article方法已实现缓存
# lxy: 上一篇、下一篇文章从缓存获取Article模型的next_article/prev_article方法已实现缓存
kwargs['next_article'] = self.object.next_article
kwargs['prev_article'] = self.object.prev_article
# 调用父类方法,保留原有上下文数据(如文章对象)
# lxy: 调用父类方法,保留原有上下文数据(如文章对象)
return super(ArticleDetailView, self).get_context_data(** kwargs)
# 定义分类详情视图类CategoryDetailView继承自ArticleListView
# lxy: 定义分类详情视图类CategoryDetailView继承自ArticleListView
class CategoryDetailView(ArticleListView):
'''
分类目录列表视图展示指定分类及子分类下的已发布文章
'''
page_type = "分类目录归档" # 页面类型标识(模板中显示该标题)
# 实现父类抽象方法:获取分类下的文章数据
# lxy: 实现父类抽象方法:获取分类下的文章数据
def get_queryset_data(self):
# 从URL路径参数获取分类slug与路由配置的<slug:category_name>对应)
# lxy: 从URL路径参数获取分类slug与路由配置的<slug:category_name>对应)
slug = self.kwargs['category_name']
# 获取分类对象不存在则返回404
# lxy: 获取分类对象不存在则返回404
category = get_object_or_404(Category, slug=slug)
categoryname = category.name # 分类名称
self.categoryname = categoryname # 保存到实例属性供后续生成缓存key和上下文使用
# 获取当前分类的所有子分类名称(含自身)
# lxy: 获取当前分类的所有子分类名称(含自身)
categorynames = list(
map(lambda c: c.name, category.get_sub_categorys()))
# 筛选条件分类名称在子分类列表中、状态为已发布status='p'
# lxy: 筛选条件分类名称在子分类列表中、状态为已发布status='p'
article_list = Article.objects.filter(
category__name__in=categorynames, status='p')
return article_list
# 实现父类抽象方法生成分类列表缓存key含分类名称和页码确保缓存唯一
# lxy: 实现父类抽象方法生成分类列表缓存key含分类名称和页码确保缓存唯一
def get_queryset_cache_key(self):
slug = self.kwargs['category_name']
category = get_object_or_404(Category, slug=slug)
@ -255,10 +255,10 @@ class CategoryDetailView(ArticleListView):
categoryname=categoryname, page=self.page_number)
return cache_key
# 重写父类的get_context_data方法添加分类相关上下文页面类型、分类名称
# lxy: 重写父类的get_context_data方法添加分类相关上下文页面类型、分类名称
def get_context_data(self, **kwargs):
categoryname = self.categoryname
# 处理分类名称(若含'/',取最后一部分,避免多级分类显示异常)
# lxy: 处理分类名称(若含'/',取最后一部分,避免多级分类显示异常)
try:
categoryname = categoryname.split('/')[-1]
except BaseException:
@ -268,32 +268,32 @@ class CategoryDetailView(ArticleListView):
return super(CategoryDetailView, self).get_context_data(** kwargs)
# 定义作者详情视图类AuthorDetailView继承自ArticleListView
# lxy: 定义作者详情视图类AuthorDetailView继承自ArticleListView
class AuthorDetailView(ArticleListView):
'''
作者文章列表视图展示指定作者发布的已发布文章
'''
page_type = '作者文章归档' # 页面类型标识
# 实现父类抽象方法生成作者文章列表缓存key
# lxy: 实现父类抽象方法生成作者文章列表缓存key
def get_queryset_cache_key(self):
from uuslug import slugify # 延迟导入slugify函数避免循环导入
# 从URL路径参数获取作者名称转换为slug格式确保缓存key统一
# lxy: 从URL路径参数获取作者名称转换为slug格式确保缓存key统一
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
# 实现父类抽象方法:获取指定作者的文章数据
# lxy: 实现父类抽象方法:获取指定作者的文章数据
def get_queryset_data(self):
# 从URL路径参数获取作者名称
# lxy: 从URL路径参数获取作者名称
author_name = self.kwargs['author_name']
# 筛选条件作者用户名匹配、类型为普通文章type='a'、状态为已发布status='p'
# lxy: 筛选条件作者用户名匹配、类型为普通文章type='a'、状态为已发布status='p'
article_list = Article.objects.filter(
author__username=author_name, type='a', status='p')
return article_list
# 重写父类的get_context_data方法添加作者相关上下文页面类型、作者名称
# lxy: 重写父类的get_context_data方法添加作者相关上下文页面类型、作者名称
def get_context_data(self, **kwargs):
author_name = self.kwargs['author_name']
kwargs['page_type'] = AuthorDetailView.page_type # 页面类型
@ -301,27 +301,27 @@ class AuthorDetailView(ArticleListView):
return super(AuthorDetailView, self).get_context_data(** kwargs)
# 定义标签详情视图类TagDetailView继承自ArticleListView
# lxy: 定义标签详情视图类TagDetailView继承自ArticleListView
class TagDetailView(ArticleListView):
'''
标签列表视图展示指定标签下的已发布文章
'''
page_type = '分类标签归档' # 页面类型标识
# 实现父类抽象方法:获取标签下的文章数据
# lxy: 实现父类抽象方法:获取标签下的文章数据
def get_queryset_data(self):
# 从URL路径参数获取标签slug
# lxy: 从URL路径参数获取标签slug
slug = self.kwargs['tag_name']
# 获取标签对象不存在则返回404
# lxy: 获取标签对象不存在则返回404
tag = get_object_or_404(Tag, slug=slug)
tag_name = tag.name # 标签名称
self.name = tag_name # 保存到实例属性
# 筛选条件标签名称匹配、状态为已发布status='p'
# lxy: 筛选条件标签名称匹配、状态为已发布status='p'
article_list = Article.objects.filter(
tags__name=tag_name, type='a', status='p')
return article_list
# 实现父类抽象方法生成标签列表缓存key含标签名称和页码
# lxy: 实现父类抽象方法生成标签列表缓存key含标签名称和页码
def get_queryset_cache_key(self):
slug = self.kwargs['tag_name']
tag = get_object_or_404(Tag, slug=slug)
@ -331,7 +331,7 @@ class TagDetailView(ArticleListView):
tag_name=tag_name, page=self.page_number)
return cache_key
# 重写父类的get_context_data方法添加标签相关上下文页面类型、标签名称
# lxy: 重写父类的get_context_data方法添加标签相关上下文页面类型、标签名称
def get_context_data(self, **kwargs):
tag_name = self.name # 从实例属性获取标签名称
kwargs['page_type'] = TagDetailView.page_type # 页面类型
@ -339,7 +339,7 @@ class TagDetailView(ArticleListView):
return super(TagDetailView, self).get_context_data(** kwargs)
# 定义文章归档视图类ArchivesView继承自ArticleListView
# lxy: 定义文章归档视图类ArchivesView继承自ArticleListView
class ArchivesView(ArticleListView):
'''
文章归档页面视图展示所有已发布文章按时间归档
@ -349,33 +349,33 @@ class ArchivesView(ArticleListView):
page_kwarg = None # 无需分页参数
template_name = 'blog/article_archives.html' # 归档页专用模板
# 实现父类抽象方法:获取所有已发布文章
# lxy: 实现父类抽象方法:获取所有已发布文章
def get_queryset_data(self):
return Article.objects.filter(status='p').all()
# 实现父类抽象方法生成归档页缓存key无页码因不分页
# lxy: 实现父类抽象方法生成归档页缓存key无页码因不分页
def get_queryset_cache_key(self):
cache_key = 'archives'
return cache_key
# 定义友情链接列表视图类LinkListView继承自Django的ListView
# lxy: 定义友情链接列表视图类LinkListView继承自Django的ListView
class LinkListView(ListView):
model = Links # 关联Links模型
template_name = 'blog/links_list.html' # 友情链接页面模板
# 重写get_queryset方法仅查询已启用的友情链接
# lxy: 重写get_queryset方法仅查询已启用的友情链接
def get_queryset(self):
return Links.objects.filter(is_enable=True)
# 定义搜索视图类EsSearchView继承自Haystack的SearchView
# lxy: 定义搜索视图类EsSearchView继承自Haystack的SearchView
class EsSearchView(SearchView):
# 重写get_context方法构建搜索结果页面的上下文数据分页、搜索关键词、拼写建议
# lxy: 重写get_context方法构建搜索结果页面的上下文数据分页、搜索关键词、拼写建议
def get_context(self):
# 构建分页器和当前页数据Haystack内置方法
# lxy: 构建分页器和当前页数据Haystack内置方法
paginator, page = self.build_page()
# 基础上下文数据:搜索关键词、搜索表单、分页数据、拼写建议
# lxy: 基础上下文数据:搜索关键词、搜索表单、分页数据、拼写建议
context = {
"query": self.query, # 搜索关键词
"form": self.form, # 搜索表单
@ -383,16 +383,16 @@ class EsSearchView(SearchView):
"paginator": paginator, # 分页器
"suggestion": None, # 拼写建议默认None
}
# 若搜索引擎支持拼写建议,获取建议词
# lxy: 若搜索引擎支持拼写建议,获取建议词
if hasattr(self.results, "query") and self.results.query.backend.include_spelling:
context["suggestion"] = self.results.query.get_spelling_suggestion()
# 添加额外上下文数据(子类可扩展)
# lxy: 添加额外上下文数据(子类可扩展)
context.update(self.extra_context())
return context
# CSRF豁免装饰器关闭CSRF验证文件上传接口通常由第三方工具调用无需CSRF令牌
# lxy: CSRF豁免装饰器关闭CSRF验证文件上传接口通常由第三方工具调用无需CSRF令牌
@csrf_exempt
def fileupload(request):
"""
@ -401,66 +401,66 @@ def fileupload(request):
:param request: HTTP请求对象
:return: 上传成功返回文件URL列表失败返回403/错误信息
"""
# 仅允许POST请求文件上传需用POST
# lxy: 仅允许POST请求文件上传需用POST
if request.method == 'POST':
# 从GET参数获取签名用于验证上传权限
# lxy: 从GET参数获取签名用于验证上传权限
sign = request.GET.get('sign', None)
if not sign:
return HttpResponseForbidden() # 无签名返回403禁止访问
# 验证签名双重SHA256加密项目SECRET_KEY与传递的sign比对防止未授权上传
# lxy: 验证签名双重SHA256加密项目SECRET_KEY与传递的sign比对防止未授权上传
if not sign == get_sha256(get_sha256(settings.SECRET_KEY)):
return HttpResponseForbidden() # 签名不匹配返回403
response = [] # 存储上传成功的文件URL
# 遍历请求中的所有上传文件(支持多文件同时上传)
# lxy: 遍历请求中的所有上传文件(支持多文件同时上传)
for filename in request.FILES:
# 生成日期字符串(按年/月/日划分上传目录,便于管理)
# lxy: 生成日期字符串(按年/月/日划分上传目录,便于管理)
timestr = timezone.now().strftime('%Y/%m/%d')
# 支持的图片格式后缀
# lxy: 支持的图片格式后缀
imgextensions = ['jpg', 'png', 'jpeg', 'bmp']
# 文件名字符串处理
# lxy: 文件名字符串处理
fname = u''.join(str(filename))
# 判断是否为图片文件(检查文件名是否包含图片后缀)
# lxy: 判断是否为图片文件(检查文件名是否包含图片后缀)
isimage = len([i for i in imgextensions if fname.find(i) >= 0]) > 0
# 定义文件保存根目录:
# - 图片文件保存到 static/files/image/年/月/日
# - 其他文件保存到 static/files/files/年/月/日
# lxy: 定义文件保存根目录:
# lxy: - 图片文件保存到 static/files/image/年/月/日
# lxy: - 其他文件保存到 static/files/files/年/月/日
base_dir = os.path.join(settings.STATICFILES, "files" if not isimage else "image", timestr)
# 若目录不存在,创建多级目录
# lxy: 若目录不存在,创建多级目录
if not os.path.exists(base_dir):
os.makedirs(base_dir)
# 生成唯一文件名UUID+原文件后缀(避免重名)
# lxy: 生成唯一文件名UUID+原文件后缀(避免重名)
savepath = os.path.normpath(os.path.join(base_dir, f"{uuid.uuid4().hex}{os.path.splitext(filename)[-1]}"))
# 安全校验:确保保存路径在基础目录内(防止路径穿越攻击)
# lxy: 安全校验:确保保存路径在基础目录内(防止路径穿越攻击)
if not savepath.startswith(base_dir):
return HttpResponse("only for post")
# 写入文件:分块写入(处理大文件上传,避免内存溢出)
# lxy: 写入文件:分块写入(处理大文件上传,避免内存溢出)
with open(savepath, 'wb+') as wfile:
for chunk in request.FILES[filename].chunks():
wfile.write(chunk)
# 若为图片文件,压缩图片(降低文件大小,提升访问速度)
# lxy: 若为图片文件,压缩图片(降低文件大小,提升访问速度)
if isimage:
from PIL import Image # 延迟导入PIL库图片处理
image = Image.open(savepath)
image.save(savepath, quality=20, optimize=True) # 质量20开启优化
# 生成文件访问URL通过static标签生成静态文件URL
# lxy: 生成文件访问URL通过static标签生成静态文件URL
url = static(savepath)
response.append(url) # 将URL添加到响应列表
# 返回URL列表字符串格式
# lxy: 返回URL列表字符串格式
return HttpResponse(response)
else:
# 非POST请求返回错误信息
# lxy: 非POST请求返回错误信息
return HttpResponse("only for post")
# 404页面未找到视图处理所有不存在的URL请求
# lxy: 404页面未找到视图处理所有不存在的URL请求
def page_not_found_view(
request,
exception,
@ -468,7 +468,7 @@ def page_not_found_view(
if exception:
logger.error(exception) # 记录异常信息到日志
url = request.get_full_path() # 获取用户访问的不存在的URL
# 渲染404错误页面传递错误信息和状态码
# lxy: 渲染404错误页面传递错误信息和状态码
return render(request,
template_name,
{'message': _('Sorry, the page you requested is not found, please click the home page to see other?'),
@ -476,9 +476,9 @@ def page_not_found_view(
status=404)
# 500服务器错误视图处理服务器内部错误
# lxy: 500服务器错误视图处理服务器内部错误
def server_error_view(request, template_name='blog/error_page.html'):
# 渲染500错误页面传递错误信息和状态码
# lxy: 渲染500错误页面传递错误信息和状态码
return render(request,
template_name,
{'message': _('Sorry, the server is busy, please click the home page to see other?'),
@ -486,21 +486,21 @@ def server_error_view(request, template_name='blog/error_page.html'):
status=500)
# 403权限拒绝视图处理无权限访问的请求
# lxy: 403权限拒绝视图处理无权限访问的请求
def permission_denied_view(
request,
exception,
template_name='blog/error_page.html'):
if exception:
logger.error(exception) # 记录异常信息到日志
# 渲染403错误页面传递错误信息和状态码
# lxy: 渲染403错误页面传递错误信息和状态码
return render(
request, template_name, {
'message': _('Sorry, you do not have permission to access this page?'),
'statuscode': '403'}, status=403)
# 清理缓存视图:清空所有缓存(用于手动刷新缓存)
# lxy: 清理缓存视图:清空所有缓存(用于手动刷新缓存)
def clean_cache_view(request):
cache.clear() # 调用缓存工具的clear方法清空所有缓存
return HttpResponse('ok') # 返回成功响应
Loading…
Cancel
Save