DjangoBlog开源代码的泛读报告 #15

Merged
pnry42fjm merged 26 commits from develop into master 4 months ago

@ -0,0 +1,19 @@
# 当该包通常是Django应用被导入时打印初始化信息
# 主要用于开发调试,确认包的加载时机和状态
print("__init__.py is running")
# 定义Django应用的默认配置类
# Django在启动时会根据此配置加载应用包括应用名称、信号注册、初始化逻辑等
# 'djangoblog.apps.DjangoblogAppConfig'表示配置类的完整路径:
# - djangoblog应用所在的包名
# - apps存放配置类的模块名
# - DjangoblogAppConfig具体的配置类继承自django.apps.AppConfig
default_app_config = 'djangoblog.apps.DjangoblogAppConfig'
# 导入pymysql库Python连接MySQL的第三方库
import pymysql
# 将pymysql伪装成MySQLdb库
# 背景早期Django默认使用MySQLdb库连接MySQL但MySQLdb不支持Python3
# 作用让Django在使用`import MySQLdb`时实际导入pymysql实现Python3环境下的MySQL连接兼容
pymysql.install_as_MySQLdb()

@ -0,0 +1,261 @@
<<<<<<< HEAD
<<<<<<< HEAD
from django import forms
from django.contrib.auth.admin import UserAdmin
from django.contrib.auth.forms import UserChangeForm
from django.contrib.auth.forms import UsernameField
from django.utils.translation import gettext_lazy as _
# Register your models here.
from .models import BlogUser
class BlogUserCreationForm(forms.ModelForm):
password1 = forms.CharField(label=_('password'), widget=forms.PasswordInput)
password2 = forms.CharField(label=_('Enter password again'), widget=forms.PasswordInput)
class Meta:
model = BlogUser
fields = ('email',)
def clean_password2(self):
# Check that the two password entries match
password1 = self.cleaned_data.get("password1")
password2 = self.cleaned_data.get("password2")
if password1 and password2 and password1 != password2:
raise forms.ValidationError(_("passwords do not match"))
return password2
def save(self, commit=True):
# Save the provided password in hashed format
user = super().save(commit=False)
user.set_password(self.cleaned_data["password1"])
if commit:
user.source = 'adminsite'
user.save()
return user
class BlogUserChangeForm(UserChangeForm):
class Meta:
model = BlogUser
fields = '__all__'
field_classes = {'username': UsernameField}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
class BlogUserAdmin(UserAdmin):
form = BlogUserChangeForm
add_form = BlogUserCreationForm
list_display = (
'id',
'nickname',
'username',
'email',
'last_login',
'date_joined',
'source')
list_display_links = ('id', 'username')
ordering = ('-id',)
=======
from django.contrib import admin
from django.urls import reverse
from django.utils.html import format_html # 用于安全地生成HTML内容
from django.utils.translation import gettext_lazy as _ # 用于国际化翻译
# 自定义批量操作:禁用评论状态
def disable_commentstatus(modeladmin, request, queryset):
queryset.update(is_enable=False) # 将选中的评论记录is_enable字段设为False
# 自定义批量操作:启用评论状态
def enable_commentstatus(modeladmin, request, queryset):
queryset.update(is_enable=True) # 将选中的评论记录is_enable字段设为True
# 为批量操作设置显示名称(支持国际化)
disable_commentstatus.short_description = _('Disable comments')
enable_commentstatus.short_description = _('Enable comments')
class CommentAdmin(admin.ModelAdmin):
list_per_page = 20 # 每页显示20条记录
list_display = (
'id',
'body', # 评论内容
'link_to_userinfo', # 自定义字段:链接到用户信息
'link_to_article', # 自定义字段:链接到文章
'is_enable', # 是否启用
'creation_time' # 创建时间
)
# 列表页中可点击跳转编辑页的字段
list_display_links = ('id', 'body', 'is_enable')
# 可筛选的字段(右侧过滤器)
list_filter = ('is_enable',)
# 编辑页排除的字段(不允许编辑,如自动生成的时间)
exclude = ('creation_time', 'last_modify_time')
# 注册自定义批量操作
actions = [disable_commentstatus, enable_commentstatus]
# 自定义列表字段:生成用户信息的编辑链接
def link_to_userinfo(self, obj):
# 获取用户模型的app标签和模型名称
info = (obj.author._meta.app_label, obj.author._meta.model_name)
# 生成用户编辑页的URL
link = reverse('admin:%s_%s_change' % info, args=(obj.author.id,))
# 返回带链接的HTML优先显示昵称无昵称则显示邮箱
return format_html(
u'<a href="%s">%s</a>' %
(link, obj.author.nickname if obj.author.nickname else obj.author.email))
# 自定义列表字段:生成文章的编辑链接
def link_to_article(self, obj):
# 获取文章模型的app标签和模型名称
info = (obj.article._meta.app_label, obj.article._meta.model_name)
# 生成文章编辑页的URL
link = reverse('admin:%s_%s_change' % info, args=(obj.article.id,))
# 返回带链接的HTML显示文章标题
return format_html(
u'<a href="%s">%s</a>' % (link, obj.article.title))
# 自定义字段的显示名称(支持国际化)
link_to_userinfo.short_description = _('User')
link_to_article.short_description = _('Article')
>>>>>>> zh_branch
=======
from django import forms
from django.contrib import admin
from django.contrib.auth import get_user_model
from django.urls import reverse
from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _
# Register your models here.
from .models import Article
class ArticleForm(forms.ModelForm):
# body = forms.CharField(widget=AdminPagedownWidget())
class Meta:
model = Article
fields = '__all__'
# 管理员动作函数 - 发布选中文章
def makr_article_publish(modeladmin, request, queryset):
"""将选中的文章状态设置为已发布"""
queryset.update(status='p')
# 管理员动作函数 - 将选中文章设为草稿
def draft_article(modeladmin, request, queryset):
"""将选中的文章状态设置为草稿"""
queryset.update(status='d')
# 管理员动作函数 - 关闭文章评论
def close_article_commentstatus(modeladmin, request, queryset):
"""关闭选中文章的评论功能"""
queryset.update(comment_status='c')
# 管理员动作函数 - 开启文章评论
def open_article_commentstatus(modeladmin, request, queryset):
"""开启选中文章的评论功能"""
queryset.update(comment_status='o')
# 设置管理员动作的显示名称
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')
class ArticlelAdmin(admin.ModelAdmin):
"""文章模型的后台管理配置"""
list_per_page = 20 # 每页显示20条记录
search_fields = ('body', 'title') # 搜索字段
form = ArticleForm # 使用自定义表单
list_display = (
'id',
'title',
'author',
'link_to_category',
'creation_time',
'views',
'status',
'type',
'article_order') # 列表页显示的字段
list_display_links = ('id', 'title') # 可点击链接的字段
list_filter = ('status', 'type', 'category') # 右侧过滤器
filter_horizontal = ('tags',) # 水平多选控件用于标签
exclude = ('creation_time', 'last_modify_time') # 排除的字段
view_on_site = True # 启用"在站点查看"功能
actions = [ # 管理员动作列表
makr_article_publish,
draft_article,
close_article_commentstatus,
open_article_commentstatus]
def link_to_category(self, obj):
"""生成分类的管理后台链接"""
info = (obj.category._meta.app_label, obj.category._meta.model_name)
link = reverse('admin:%s_%s_change' % info, args=(obj.category.id,))
return format_html(u'<a href="%s">%s</a>' % (link, obj.category.name))
link_to_category.short_description = _('category') # 设置列显示名称
def get_form(self, request, obj=None, **kwargs):
"""自定义表单,限制作者只能选择超级用户"""
form = super(ArticlelAdmin, self).get_form(request, obj, **kwargs)
form.base_fields['author'].queryset = get_user_model(
).objects.filter(is_superuser=True)
return form
def save_model(self, request, obj, form, change):
"""保存模型时的自定义逻辑"""
super(ArticlelAdmin, self).save_model(request, obj, form, change)
def get_view_on_site_url(self, obj=None):
"""获取"在站点查看"的URL"""
if obj:
url = obj.get_full_url() # 文章的完整URL
return url
else:
from djangoblog.utils import get_current_site
site = get_current_site().domain # 站点域名
return site
class TagAdmin(admin.ModelAdmin):
"""标签模型的后台管理配置"""
exclude = ('slug', 'last_mod_time', 'creation_time') # 排除自动生成的字段
class CategoryAdmin(admin.ModelAdmin):
"""分类模型的后台管理配置"""
list_display = ('name', 'parent_category', 'index') # 列表显示字段
exclude = ('slug', 'last_mod_time', 'creation_time') # 排除自动生成的字段
class LinksAdmin(admin.ModelAdmin):
"""友情链接模型的后台管理配置"""
exclude = ('last_mod_time', 'creation_time') # 排除时间字段
class SideBarAdmin(admin.ModelAdmin):
"""侧边栏模型的后台管理配置"""
list_display = ('name', 'content', 'is_enable', 'sequence') # 列表显示字段
exclude = ('last_mod_time', 'creation_time') # 排除时间字段
class BlogSettingsAdmin(admin.ModelAdmin):
"""博客设置模型的后台管理配置"""
pass # 使用默认管理配置
>>>>>>> hyt_branch

@ -0,0 +1,103 @@
# 导入Django Admin相关核心组件
from django.contrib.admin import AdminSite # Django Admin站点基类
from django.contrib.admin.models import LogEntry # 管理员操作日志模型
from django.contrib.sites.admin import SiteAdmin # 站点管理的默认Admin配置
from django.contrib.sites.models import Site # Django内置的站点模型用于多站点管理
# 导入各应用的Admin配置和数据模型
from accounts.admin import * # 账户相关的Admin配置
from blog.admin import * # 博客核心功能文章、分类等的Admin配置
from blog.models import * # 博客核心数据模型
from comments.admin import * # 评论功能的Admin配置
from comments.models import * # 评论相关数据模型
from djangoblog.logentryadmin import LogEntryAdmin # 自定义的操作日志Admin配置
from oauth.admin import * # 第三方登录OAuth的Admin配置
from oauth.models import * # OAuth相关数据模型
from owntracks.admin import * # 位置追踪OwnTracks的Admin配置
from owntracks.models import *# OwnTracks相关数据模型
from servermanager.admin import * # 服务器管理的Admin配置
from servermanager.models import * # 服务器管理相关数据模型
class DjangoBlogAdminSite(AdminSite):
"""
自定义的Django Admin站点类继承自Django内置的AdminSite
作用通过重写基类属性和方法定制Admin后台的外观和权限控制
"""
# 定制Admin站点的页面头部标题显示在登录页和后台顶部导航栏
site_header = 'djangoblog administration'
# 定制Admin站点的页面标题显示在浏览器标签页
site_title = 'djangoblog site admin'
def __init__(self, name='admin'):
"""
初始化自定义Admin站点
:param name: 站点名称默认'admin'与Django默认Admin站点名称保持一致避免路由冲突
"""
super().__init__(name) # 调用父类构造方法初始化
def has_permission(self, request):
"""
重写权限检查方法控制谁能访问Admin后台
:param request: HTTP请求对象包含当前用户信息
:return: 布尔值True表示允许访问False表示拒绝访问
此处限制仅超级用户is_superuser可访问比默认的is_staff更严格
"""
return request.user.is_superuser
# 以下为注释掉的自定义URL示例可根据需求启用
# def get_urls(self):
# """
# 扩展Admin站点的URL路由添加自定义功能入口
# """
# # 先获取父类默认的URL配置
# urls = super().get_urls()
# # 导入URL路径处理和自定义视图
# from django.urls import path
# from blog.views import refresh_memcache # 示例:缓存刷新视图
#
# # 定义自定义URL规则使用admin_view()包装确保权限检查
# my_urls = [
# path('refresh/', self.admin_view(refresh_memcache), name="refresh"),
# ]
# # 合并默认URL和自定义URL自定义URL优先
# return urls + my_urls
# 实例化自定义的Admin站点名称为'admin'与Django默认Admin站点名称一致接管后台
admin_site = DjangoBlogAdminSite(name='admin')
# 注册数据模型与对应的Admin配置到自定义Admin站点
# 博客核心内容
admin_site.register(Article, ArticlelAdmin) # 文章模型 + 其Admin配置
admin_site.register(Category, CategoryAdmin) # 分类模型 + 其Admin配置
admin_site.register(Tag, TagAdmin) # 标签模型 + 其Admin配置
admin_site.register(Links, LinksAdmin) # 友情链接模型 + 其Admin配置
admin_site.register(SideBar, SideBarAdmin) # 侧边栏模型 + 其Admin配置
admin_site.register(BlogSettings, BlogSettingsAdmin) # 博客设置模型 + 其Admin配置
# 服务器管理
admin_site.register(commands, CommandsAdmin) # 命令模型 + 其Admin配置
admin_site.register(EmailSendLog, EmailSendLogAdmin)# 邮件发送日志模型 + 其Admin配置
# 账户管理
admin_site.register(BlogUser, BlogUserAdmin) # 自定义用户模型 + 其Admin配置
# 评论管理
admin_site.register(Comment, CommentAdmin) # 评论模型 + 其Admin配置
# OAuth第三方登录
admin_site.register(OAuthUser, OAuthUserAdmin) # OAuth用户模型 + 其Admin配置
admin_site.register(OAuthConfig, OAuthConfigAdmin) # OAuth配置模型 + 其Admin配置
# 位置追踪
admin_site.register(OwnTrackLog, OwnTrackLogsAdmin) # 位置日志模型 + 其Admin配置
# 站点管理Django内置
admin_site.register(Site, SiteAdmin) # 站点模型 + Django默认的SiteAdmin配置
# 操作日志管理
admin_site.register(LogEntry, LogEntryAdmin) # 管理员操作日志模型 + 自定义Admin配置

@ -0,0 +1,76 @@
<<<<<<< HEAD
<<<<<<< HEAD
<<<<<<< HEAD
# 导入Django的AppConfig类用于配置Django应用的生命周期和元数据
from django.apps import AppConfig
class DjangoblogAppConfig(AppConfig):
"""
Django博客应用djangoblog的配置类用于定义应用的核心配置和生命周期钩子
作用
1. 配置应用的数据库主键生成规则
2. 标识应用的唯一名称
3. 定义应用就绪后的初始化逻辑如加载插件
"""
# 配置Django模型默认的自增主键字段类型
# BigAutoField是64位整数型自增字段支持更大的主键范围适用于数据量较大的博客
# 替代默认的AutoField32位整数避免数据量增长后主键溢出问题
default_auto_field = 'django.db.models.BigAutoField'
# 应用的唯一名称,必须与项目中应用的目录名一致(此处为'djangoblog'
# Django通过该名称识别应用用于注册路由、加载模型等核心操作
name = 'djangoblog'
def ready(self):
"""
Django应用就绪后的钩子方法在应用完全加载并初始化后自动调用
执行时机
- 项目启动时如runservercelery启动
- 应用注册表app registry完成所有应用加载后
注意该方法可能会被多次调用如开发环境自动重载时需确保逻辑可重入
核心功能
调用插件加载函数在应用就绪后初始化所有已激活的插件
"""
# 调用父类的ready()方法确保Django默认的应用就绪逻辑正常执行
super().ready()
# 导入并执行插件加载函数:
# 1. 从当前应用djangoblog的plugin_manage.loader模块中导入load_plugins函数
# 2. 调用load_plugins()触发插件动态加载(如导入插件模块、初始化插件实例)
# 此处是插件系统与Django应用生命周期的绑定点确保插件在应用就绪后启动
from .plugin_manage.loader import load_plugins
load_plugins()
from django.apps import AppConfig
class AccountsConfig(AppConfig):
name = 'accounts'
=======
from django.apps import AppConfig
class CommentsConfig(AppConfig):
# 配置应用的名称,对应项目中该应用的目录名)
# Django通过这个名称识别和管理该应用
name = 'comments'
>>>>>>> zh_branch
=======
from django.apps import AppConfig
class BlogConfig(AppConfig):
"""博客应用配置类
这个类用于配置Django博客应用的基本信息
它继承自Django的AppConfig基类用于定义应用的元数据和行为
"""
# 应用的完整Python路径Django使用这个名称来识别应用
name = 'blog'
>>>>>>> hyt_branch

@ -1,79 +1,83 @@
{% load blog_tags %} // 加载自定义博客相关的自定义模板标签库
{% load cache %} // 加载缓存功能相关的模板标签库
{% load i18n %} // 加载国际化相关的模板标签库
{% load blog_tags %}
{% load cache %}
{% load i18n %}
<!-- 文章主体容器包含唯一ID和样式类 -->
<article id="post-{{ article.pk }} "
class="post-{{ article.pk }} post type-post status-publish format-standard hentry">// 加载国际化相关的模板标签库
class="post-{{ article.pk }} post type-post status-publish format-standard hentry">
<header class="entry-header"> <!-- 文章头部区域 -->
<header class="entry-header">// 文章头部区域,包含标题等信息
<h1 class="entry-title">// 文章标题元素
{% if isindex %} // 判断当前是否为索引页(文章列表页)
{% if article.article_order > 0 %} // 若为索引页判断文章是否有置顶顺序大于0表示置顶
<h1 class="entry-title"> <!-- 文章标题 -->
{% if isindex %} <!-- 判断是否为首页/列表页 -->
{% if article.article_order > 0 %} <!-- 若文章有置顶权重大于0 -->
<!-- 显示带"置顶"标识的标题链接 -->
<a href="{{ article.get_absolute_url }}"
rel="bookmark">【{% trans 'pin to top' %}】{{ article.title }}</a>// 显示带"置顶"标识的文章标题
rel="bookmark">【{% trans 'pin to top' %}】{{ article.title }}</a>
{% else %}
<!-- 普通标题链接 -->
<a href="{{ article.get_absolute_url }}"
rel="bookmark">{{ article.title }}</a>// 显示普通文章标题
rel="bookmark">{{ article.title }}</a>
{% endif %}
{% else %}
<!-- 非列表页直接显示标题(不添加链接) -->
{{ article.title }}
{% endif %}
</h1>
<div class="comments-link">
{% if article.comment_status == "o" and open_site_comment %}
<div class="comments-link"> <!-- 评论链接区域 -->
{% if article.comment_status == "o" and open_site_comment %} <!-- 若评论开启且网站允许评论 -->
<!-- 评论链接,指向文章详情页的评论区 -->
<a href="{{ article.get_absolute_url }}#comments" class="ds-thread-count" data-thread-key="3815"
rel="nofollow">// 链接添加nofollow属性告诉搜索引擎不要追踪此链接
<span class="leave-reply">// 评论数量显示容器
{% if article.comment_set and article.comment_set.count %}
{{ article.comment_set.count }} {% trans 'comments' %}// 检查是否有评论且评论数量存在
rel="nofollow">
<span class="leave-reply">
{% if article.comment_set and article.comment_set.count %} <!-- 若存在评论 -->
{{ article.comment_set.count }} {% trans 'comments' %} <!-- 显示评论数量 -->
{% else %}
{% trans 'comment' %}// 显示"comment"文本(国际化)
{% trans 'comment' %} <!-- 显示"评论"文本 -->
{% endif %}
</span>
</a>
{% endif %}
<div style="float:right">// 阅读量显示容器,设置右浮动
{{ article.views }} views// 显示文章阅读量和"views"文本
<!-- 右侧显示文章阅读量 -->
<div style="float:right">
{{ article.views }} views
</div>
</div><!-- .comments-link -->
<br/>
{% if article.type == 'a' %} // 检查文章类型是否为文章
{% if not isindex %} // 如果当前不是索引页(即文章详情页)
{% cache 36000 breadcrumb article.pk %}// 缓存面包屑导航有效期10小时36000秒以文章主键作为缓存键
{% load_breadcrumb article %}// 调用自定义标签加载文章的面包屑导航
{% if article.type == 'a' %} <!-- 若文章类型为普通文章(假设'a'代表文章) -->
{% if not isindex %} <!-- 非列表页时显示面包屑导航 -->
<!-- 缓存面包屑导航10小时36000秒以文章ID作为缓存键 -->
{% cache 36000 breadcrumb article.pk %}
{% load_breadcrumb article %} <!-- 调用自定义标签生成面包屑 -->
{% endcache %}
{% endif %}
{% endif %}
</header><!-- .entry-header -->
<div class="entry-content" itemprop="articleBody">
{% if isindex %}
{{ article.body|custom_markdown|escape|truncatechars_content }}// 显示经过markdown处理、转义并截断的文章内容
<div class="entry-content" itemprop="articleBody"> <!-- 文章内容区域,标记为文章主体 -->
{% if isindex %} <!-- 列表页显示 -->
<!-- 显示经过自定义markdown处理、转义并截断的内容 -->
{{ article.body|custom_markdown|escape|truncatechars_content }}
<!-- 阅读更多链接 -->
<p class='read-more'><a
href=' {{ article.get_absolute_url }}'>Read more</a></p>
{% else %}
{% if article.show_toc %}// 检查文章是否设置显示目录
{% get_markdown_toc article.body as toc %}// 调用自定义标签获取文章内容的markdown目录并赋值给toc变量
<b>{% trans 'toc' %}:</b>
{{ toc|safe }}// 安全地显示目录内容允许HTML渲染
<hr class="break_line"/>
{% else %} <!-- 详情页显示 -->
{% if article.show_toc %} <!-- 若文章设置显示目录 -->
<!-- 获取markdown内容中的目录 -->
{% get_markdown_toc article.body as toc %}
<b>{% trans 'toc' %}:</b> <!-- 显示"目录"标签 -->
{{ toc|safe }} <!-- 安全渲染目录HTML -->
<hr class="break_line"/> <!-- 分隔线 -->
{% endif %}
<div class="article">
{{ article.body|custom_markdown|escape }}// 显示经过markdown处理和转义的完整文章内容
<!-- 显示完整的经过markdown处理和转义的文章内容 -->
{{ article.body|custom_markdown|escape }}
</div>
{% endif %}
</div><!-- .entry-content -->
{% load_article_metas article user %}// 调用自定义标签加载文章的元数据(如作者、发布时间等),传入文章对象和用户对象
<!-- 加载文章元数据(如作者、发布时间、分类等),调用自定义标签 -->
{% load_article_metas article user %}
</article><!-- #post -->

@ -2,57 +2,62 @@
{% load blog_tags %}
<footer class="entry-meta">
{% trans 'posted in' %}
<footer class="entry-meta"> <!-- 文章元数据区域(底部信息栏) -->
{% trans 'posted in' %} <!-- 翻译为“发布于” -->
<!-- 文章所属分类链接 -->
<a href="{{ article.category.get_absolute_url }}" rel="category tag">{{ article.category.name }}</a>
<!-- 文章分类链接 -->
{% if article.type == 'a' %}
{% if article.tags.all %}
<!-- 如果是文章类型且有标签,则显示标签 -->
{% trans 'and tagged' %}
</a> <!-- 此处可能为多余闭合标签,需注意语法正确性 -->
{% if article.type == 'a' %} <!-- 若文章类型为普通文章(假设'a'代表文章) -->
{% if article.tags.all %} <!-- 若文章有关联标签 -->
{% trans 'and tagged' %} <!-- 翻译为“并标记为” -->
<!-- 循环输出所有标签 -->
{% for t in article.tags.all %}
<a href="{{ t.get_absolute_url }}" rel="tag">{{ t.name }}</a>
{% if t != article.tags.all.last %}
, <!-- 除最后一个标签外,其他标签后加逗号 -->
<a href="{{ t.get_absolute_url }}" rel="tag">{{ t.name }}</a> <!-- 标签链接 -->
{% if t != article.tags.all.last %} <!-- 除最后一个标签外,添加逗号分隔 -->
,
{% endif %}
{% endfor %}
{% endif %}
{% endif %}
.{% trans 'by ' %}
<span class="by-author">
<span class="author vcard">
<a class="url fn n"
href="{{ article.author.get_absolute_url }}"
{% blocktranslate %}
title="View all articles published by {{ article.author.username }}"
{% endblocktranslate %}
rel="author">
<!-- 作者链接及信息 -->
<span itemprop="author" itemscope itemtype="http://schema.org/Person">
<!-- 使用schema.org定义作者信息 -->
<span itemprop="name">
<!-- 修正移除了重复的itemprop="publisher" -->
{{ article.author.username }}
</span>
</span>
</a> <!-- 修正闭合a标签 -->
.{% trans 'by ' %} <!-- 翻译为“作者:” -->
<span class="by-author"> <!-- 作者信息容器 -->
<span class="author vcard"> <!-- 符合hCard微格式的作者信息 -->
<!-- 作者主页链接 -->
<a class="url fn n" href="{{ article.author.get_absolute_url }}"
{% blocktranslate %} <!-- 国际化块,支持变量翻译 -->
title="View all articles published by {{ article.author.username }}" <!-- 鼠标悬停提示:查看该作者所有文章 -->
{% endblocktranslate %}
rel="author"> <!-- 标记为作者链接 -->
<!-- 符合Schema.org规范的作者信息 -->
<span itemprop="author" itemscope itemtype="http://schema.org/Person">
<span itemprop="name" itemprop="publisher"> <!-- 作者名称(同时作为发布者) -->
{{ article.author.username }} <!-- 显示作者用户名 -->
</span>
</span> <!-- 修正闭合by-author的span标签 -->
{% trans 'on' %}
<a href="{{ article.get_absolute_url }}"
title="{% datetimeformat article.pub_time %}"
itemprop="datePublished"
content="{% datetimeformat article.pub_time %}"
rel="bookmark">
<!-- 文章发布时间链接 -->
<time class="entry-date updated" datetime="{{ article.pub_time }}">
{% datetimeformat article.pub_time %}
</time>
{% if user.is_superuser %}
<a href="{{ article.get_admin_url }}">{% trans 'edit' %}</a>
<!-- 管理员编辑链接 -->
{% endif %}
</a> <!-- 修正闭合时间链接的a标签 -->
</footer><!-- .entry-meta -->
</span>
</a>
</span>
{% trans 'on' %} <!-- 翻译为“发布时间:” -->
<!-- 文章详情页链接(带发布时间信息) -->
<a href="{{ article.get_absolute_url }}"
title="{% datetimeformat article.pub_time %}" <!-- 鼠标悬停显示格式化的发布时间 -->
itemprop="datePublished" content="{% datetimeformat article.pub_time %}" <!-- Schema.org发布时间属性 -->
rel="bookmark"> <!-- 标记为永久链接 -->
<!-- 发布时间标签符合HTML5时间格式 -->
<time class="entry-date updated"
datetime="{{ article.pub_time }}"> <!-- datetime属性为机器可读格式 -->
{% datetimeformat article.pub_time %}</time> <!-- 显示格式化的发布时间(调用自定义标签) -->
{% if user.is_superuser %} <!-- 若当前用户是超级管理员 -->
<a href="{{ article.get_admin_url }}">{% trans 'edit' %}</a> <!-- 显示编辑链接(指向后台编辑页) -->
{% endif %}
</span>
</footer><!-- .entry-meta --> <!-- 元数据区域结束 -->

@ -1,21 +1,28 @@
{% load i18n %}
<nav id="nav-below" class="navigation" role="navigation"> // 定义文章导航的 nav 元素
<h3 class="assistive-text"> // 辅助性标题,用于无障碍访问等
{% trans 'article navigation' %} // 翻译显示“文章导航”
<!-- 文章导航区域,用于分页导航 -->
<nav id="nav-below" class="navigation" role="navigation">
<h3 class="assistive-text">
{% trans 'article navigation' %} <!-- 翻译为“文章导航”,供辅助设备识别 -->
</h3>
{% if page_obj.has_next and next_url %} // 判断是否有下一页且存在下一页 URL
<!-- 若存在下一页且有下一页URL显示“更早的文章”链接 -->
{% if page_obj.has_next and next_url %}
<div class="nav-previous">
<a href="{{ next_url }}"> // 链接到下一页 URL
<span class="meta-nav">&larr;</span> {% trans 'earlier articles' %} // 翻译显示“更早的文章”
<a href="{{ next_url }}">
<span class="meta-nav">&larr;</span> <!-- 左箭头图标 -->
{% trans 'earlier articles' %} <!-- 翻译为“更早的文章” -->
</a>
</div>
{% endif %}
{% if page_obj.has_previous and previous_url %} // 判断是否有上一页且存在上一页 URL
<!-- 若存在上一页且有上一页URL显示“更新的文章”链接 -->
{% if page_obj.has_previous and previous_url %}
<div class="nav-next">
<a href="{{ previous_url }}"> // 链接到上一页 URL
{% trans 'newer articles' %} // 翻译显示“更新的文章”
<span class="meta-nav"></span>
<a href="{{ previous_url }}">
{% trans 'newer articles' %} <!-- 翻译为“更新的文章” -->
<span class="meta-nav"></span> <!-- 右箭头图标 -->
</a>
</div>
{% endif %}
</nav> <!-- .navigation --> // 结束 nav 元素,注释说明是导航部分
</nav><!-- .navigation --> <!-- 导航区域结束 -->

@ -1,33 +1,20 @@
{% load i18n %} <!-- 加载国际化标签库,用于实现多语言支持 -->
<!-- 仅当存在标签数据时才渲染整个标签面板 -->
{% if article_tags_list %}
<!-- 标签面板容器使用Bootstrap的panel组件样式 -->
<div class="panel panel-default">
<!-- 面板标题栏 -->
<div class="panel-heading">
{% trans 'tags' %} <!-- 显示"标签"文本,支持多语言翻译 -->
{% load i18n %}
{% if article_tags_list %} <!-- 判断文章标签列表是否存在,存在则渲染标签面板 -->
<div class="panel panel-default"> <!-- 标签面板容器,使用默认样式的面板组件 -->
<div class="panel-heading"> <!-- 面板头部区域,显示标题 -->
{% trans 'tags' %} <!-- 翻译“tags”为对应语言如中文“标签”作为面板标题 -->
</div>
<div class="panel-body"> <!-- 面板内容区域,用于放置标签列表 -->
<!-- 面板内容区域 -->
<div class="panel-body">
<!-- 循环遍历标签列表数据
每个标签包含: 链接(url)、文章数量(count)、标签对象(tag)、样式颜色(color) -->
{% for url, count, tag, color in article_tags_list %}
<!-- 标签链接元素
- 使用label-{{ color }}动态应用不同颜色样式
- 添加margin样式使标签间保持适当间距
- title属性显示该标签下的文章数量支持复数形式翻译 -->
<a class="label label-{{ color }}"
style="display: inline-block; margin: 0 5px 5px 0;"
href="{{ url }}"
title="{% blocktranslate with tag_name=tag.name count=count %}
{{ count }} article tagged with {{ tag_name }}
{% endblocktranslate %}">
{% for url,count,tag,color in article_tags_list %} <!-- 循环遍历标签列表,获取每个标签的链接、数量、标签对象、颜色 -->
<!-- 标签链接使用label组件样式颜色由循环变量color控制 -->
<a class="label label-{{ color }}" style="display: inline-block;" href="{{ url }}"
title="{{ tag.name }}"> <!-- 鼠标悬停时显示标签名称 -->
{{ tag.name }} <!-- 显示标签名称 -->
<span class="badge">{{ count }}</span> <!-- 显示该标签关联的文章数量(徽章样式) -->
<span class="badge">{{ count }}</span> <!-- 显示该标签下的文章数量用badge组件样式包裹 -->
</a>
{% endfor %}
</div>
</div>
{% endif %}
{% endfor %} <!-- 标签循环结束 -->
</div> <!-- 面板内容区域结束 -->
</div> <!-- 标签面板容器结束 -->
{% endif %} <!-- 标签列表存在判断结束 -->

@ -0,0 +1,95 @@
# 导入Python标准库中的logging模块用于实现插件运行过程中的日志记录功能
# 日志可以帮助开发者追踪插件的运行状态、排查错误等
import logging
# 创建一个日志记录器实例,其名称与当前模块(__name__)绑定
# 这样可以确保日志信息能够准确关联到插件模块,便于日志的分类和筛选
logger = logging.getLogger(__name__)
class BasePlugin:
"""
插件系统的基类抽象基类角色所有自定义插件都必须继承此类
该类的核心作用是
1. 定义插件必须包含的元数据规范名称描述版本
2. 提供插件初始化和钩子注册的统一流程
3. 封装获取插件信息的通用方法
子类通过继承此类并实现特定方法即可快速接入插件系统
"""
# 插件元数据字段(子类必须显式赋值,否则初始化会失败)
PLUGIN_NAME = None # 插件的唯一标识名称,用于在系统中区分不同插件,例如"DataCleanPlugin"
PLUGIN_DESCRIPTION = None # 插件功能的详细描述,说明插件的作用和使用场景,例如"用于清洗CSV格式的原始数据"
PLUGIN_VERSION = None # 插件的版本号,遵循语义化版本规范(如"1.0.0"),用于版本管理和兼容性判断
def __init__(self):
"""
插件实例的构造方法负责插件的初始化流程控制
执行逻辑
1. 首先验证子类是否完整实现了元数据名称描述版本
2. 若元数据不完整抛出ValueError异常阻止实例化
3. 元数据验证通过后依次调用初始化方法和钩子注册方法
确保插件在使用前完成必要的准备工作
"""
# 使用all()函数检查三个元数据字段是否都有值非None
# 若存在任何一个未定义的字段,触发异常
if not all([self.PLUGIN_NAME, self.PLUGIN_DESCRIPTION, self.PLUGIN_VERSION]):
raise ValueError("Plugin metadata (PLUGIN_NAME, PLUGIN_DESCRIPTION, PLUGIN_VERSION) must be defined.")
# 调用插件初始化方法,执行子类自定义的初始化逻辑
self.init_plugin()
# 调用钩子注册方法,让子类注册需要监听的系统事件
self.register_hooks()
def init_plugin(self):
"""
插件初始化的具体实现方法用于执行插件启动前的准备工作
基类默认实现
- 输出一条INFO级别的日志提示插件已完成初始化
子类可重写此方法实现特定逻辑例如
- 加载配置文件
- 建立数据库连接
- 初始化缓存数据结构等
注意重写时若需要保留默认日志可通过super().init_plugin()调用父类方法
"""
# 记录插件初始化成功的日志,包含插件名称便于追踪
logger.info(f'{self.PLUGIN_NAME} initialized.')
def register_hooks(self):
"""
插件钩子注册方法用于将插件功能与系统事件关联
钩子Hook机制说明
系统在运行过程中会触发一系列事件"数据处理前""任务完成后"
插件通过注册钩子可以在特定事件发生时自动执行对应逻辑
基类默认实现空方法pass
子类需根据自身功能重写此方法例如
- 调用系统提供的register_hook()方法注册事件回调
- 定义需要监听的事件类型和对应的处理函数
"""
pass
def get_plugin_info(self):
"""
获取插件元数据的统一接口用于系统展示或管理插件信息
返回值说明
- 字典类型包含三个键值对
- 'name'对应PLUGIN_NAME
- 'description'对应PLUGIN_DESCRIPTION
- 'version'对应PLUGIN_VERSION
应用场景
- 插件管理界面展示插件列表
- 系统启动时收集所有插件信息进行校验
- 插件间依赖关系判断时获取版本信息
"""
return {
'name': self.PLUGIN_NAME,
'description': self.PLUGIN_DESCRIPTION,
'version': self.PLUGIN_VERSION
}

@ -0,0 +1,207 @@
# 导入必要模块
import _thread # 用于创建多线程,处理异步任务(如发送邮件)
import logging # 日志记录模块,记录信号处理过程中的关键信息和错误
import django.dispatch # Django信号系统用于定义和发送自定义信号
from django.conf import settings # 导入Django项目配置
from django.contrib.admin.models import LogEntry # 管理员操作日志模型
from django.contrib.auth.signals import user_logged_in, user_logged_out # Django内置的用户登录/登出信号
from django.core.mail import EmailMultiAlternatives # 用于发送HTML格式的邮件
from django.db.models.signals import post_save # Django模型保存后的信号
from django.dispatch import receiver # 用于注册信号接收器
# 导入项目内部模块
from comments.models import Comment # 评论模型
from comments.utils import send_comment_email # 发送评论通知邮件的工具函数
from djangoblog.spider_notify import SpiderNotify # 搜索引擎推送工具类
from djangoblog.utils import ( # 项目工具函数
cache, # 缓存操作对象
expire_view_cache, # 清除视图缓存
delete_sidebar_cache, # 清除侧边栏缓存
delete_view_cache # 清除指定视图缓存
)
from djangoblog.utils import get_current_site # 获取当前站点信息
from oauth.models import OAuthUser # OAuth用户模型
# 创建当前模块的日志记录器,用于记录信号处理相关日志
logger = logging.getLogger(__name__)
# 定义自定义信号OAuth用户登录信号
# 触发时机当用户通过OAuth第三方登录成功登录时
# 参数:['id'] 表示信号会携带OAuthUser的id
oauth_user_login_signal = django.dispatch.Signal(['id'])
# 定义自定义信号:发送邮件信号
# 触发时机:需要发送邮件时(解耦邮件发送逻辑,便于多处调用)
# 参数:['emailto', 'title', 'content'] 分别表示收件人、邮件标题、邮件内容
send_email_signal = django.dispatch.Signal(['emailto', 'title', 'content'])
@receiver(send_email_signal)
def send_email_signal_handler(sender, **kwargs):
"""
发送邮件信号的接收器处理实际的邮件发送逻辑
:param sender: 信号发送者通常无需关注
:param kwargs: 信号携带的参数emailto, title, content
"""
# 从信号参数中提取邮件信息
emailto = kwargs['emailto'] # 收件人列表
title = kwargs['title'] # 邮件标题
content = kwargs['content'] # 邮件内容HTML格式
# 创建HTML格式邮件对象
# from_email发件人从项目配置中获取
# to收件人列表
msg = EmailMultiAlternatives(
title,
content,
from_email=settings.DEFAULT_FROM_EMAIL,
to=emailto
)
msg.content_subtype = "html" # 声明邮件内容为HTML格式
# 记录邮件发送日志(存入数据库)
from servermanager.models import EmailSendLog # 导入邮件发送日志模型
log = EmailSendLog()
log.title = title
log.content = content
log.emailto = ','.join(emailto) # 将收件人列表转为字符串存储
try:
# 发送邮件,返回成功发送的数量
result = msg.send()
log.send_result = result > 0 # 若成功发送数量>0标记为发送成功
except Exception as e:
# 发送失败时记录错误日志
logger.error(f"失败邮箱号: {emailto}, {e}")
log.send_result = False # 标记为发送失败
log.save() # 保存日志记录到数据库
@receiver(oauth_user_login_signal)
def oauth_user_login_signal_handler(sender, **kwargs):
"""
OAuth用户登录信号的接收器处理第三方登录后的后续操作
:param sender: 信号发送者
:param kwargs: 信号携带的参数idOAuthUser的id
"""
id = kwargs['id'] # 获取OAuth用户ID
oauthuser = OAuthUser.objects.get(id=id) # 查询对应的OAuth用户对象
# 获取当前站点域名(用于判断头像是否为本站资源)
site = get_current_site().domain
# 若用户头像存在且不是本站资源如第三方平台的头像URL则下载并保存到本地
if oauthuser.picture and not oauthuser.picture.find(site) >= 0:
from djangoblog.utils import save_user_avatar # 导入保存用户头像的工具函数
oauthuser.picture = save_user_avatar(oauthuser.picture) # 下载并替换头像URL为本地路径
oauthuser.save() # 保存更新后的用户信息
# 清除侧边栏缓存(用户登录状态可能影响侧边栏展示内容)
delete_sidebar_cache()
@receiver(post_save)
def model_post_save_callback(
sender,
instance,
created,
raw,
using,
update_fields,
**kwargs):
"""
Django模型保存后post_save信号的接收器处理模型保存后的联动操作
:param sender: 发送信号的模型类
:param instance: 被保存的模型实例
:param created: 是否为新创建的记录True表示新建False表示更新
:param raw: 是否为原始保存如通过loaddata导入数据时为True
:param using: 使用的数据库别名
:param update_fields: 被更新的字段列表None表示全量更新
:param kwargs: 其他参数
"""
clearcache = False # 标记是否需要清除缓存
# 若保存的是管理员操作日志LogEntry直接返回无需处理
if isinstance(instance, LogEntry):
return
# 若模型实例有get_full_url方法通常表示是可被搜索引擎收录的内容如文章
if 'get_full_url' in dir(instance):
# 判断是否仅更新了浏览量字段views
is_update_views = update_fields == {'views'}
# 非测试环境且不是仅更新浏览量时,通知搜索引擎更新内容
if not settings.TESTING and not is_update_views:
try:
notify_url = instance.get_full_url() # 获取内容的完整URL
SpiderNotify.baidu_notify([notify_url]) # 向百度搜索引擎推送更新
except Exception as ex:
logger.error("notify spider", ex) # 推送失败时记录错误日志
# 若不是仅更新浏览量,标记需要清除缓存
if not is_update_views:
clearcache = True
# 若保存的是评论Comment实例
if isinstance(instance, Comment):
# 仅处理已启用的评论is_enable=True
if instance.is_enable:
# 获取评论所属文章的URL路径
path = instance.article.get_absolute_url()
# 获取当前站点域名
site = get_current_site().domain
# 处理域名中的端口(若有),仅保留主域名
if site.find(':') > 0:
site = site[0:site.find(':')]
# 清除文章详情页的视图缓存(评论更新后页面内容需同步更新)
expire_view_cache(
path,
servername=site,
serverport=80,
key_prefix='blogdetail'
)
# 清除SEO处理器缓存评论可能影响页面SEO信息
if cache.get('seo_processor'):
cache.delete('seo_processor')
# 清除该文章的评论缓存
comment_cache_key = 'article_comments_{id}'.format(id=instance.article.id)
cache.delete(comment_cache_key)
# 清除侧边栏缓存(侧边栏可能包含最新评论等动态内容)
delete_sidebar_cache()
# 清除评论列表视图的缓存
delete_view_cache('article_comments', [str(instance.article.pk)])
# 启动新线程异步发送评论通知邮件(避免阻塞主请求)
_thread.start_new_thread(send_comment_email, (instance,))
# 若标记需要清除缓存,则执行缓存清除
if clearcache:
cache.clear()
@receiver(user_logged_in) # 注册为用户登录信号的接收器
@receiver(user_logged_out) # 同时注册为用户登出信号的接收器
def user_auth_callback(sender, request, user, **kwargs):
"""
用户登录/登出信号的接收器处理认证状态变化后的操作
:param sender: 信号发送者
:param request: HTTP请求对象
:param user: 当前用户对象
:param kwargs: 其他参数
"""
# 若用户存在且用户名有效
if user and user.username:
logger.info(user) # 记录用户登录/登出日志
delete_sidebar_cache() # 清除侧边栏缓存(登录状态可能影响侧边栏内容,如显示用户名)
# cache.clear() # 注释:可根据需求开启全量缓存清除(通常侧边栏缓存足够)

@ -1,33 +1,25 @@
<!-- 面包屑导航列表使用Schema.org规范标记提升SEO和结构化数据识别 -->
<ul itemscope itemtype="https://schema.org/BreadcrumbList" class="breadcrumb">
<!--
面包屑导航容器使用schema.org的BreadcrumbList类型定义结构化数据
class="breadcrumb"通常用于Bootstrap样式的面包屑导航
-->
{% for name,url in names %}
<!-- 循环输出面包屑导航的每一级(除最后一级) -->
{% for name,url in names %} <!-- 循环遍历面包屑导航的每一级(除最后一级) -->
<!-- 每一级导航项符合Schema.org的ListItem类型 -->
<li itemprop="itemListElement" itemscope
itemtype="https://schema.org/ListItem">
<!-- 每一项使用schema.org的ListItem类型定义 -->
<!-- 导航项链接itemprop="item"标记链接地址 -->
<a href="{{ url }}" itemprop="item" >
<!-- 链接地址itemprop="item"关联到具体资源 -->
<span itemprop="name">{{ name }}</span>
<!-- 显示的名称itemprop="name"定义列表项名称 -->
</a>
<span itemprop="name">{{ name }}</span></a> <!-- 导航项名称itemprop="name"标记 -->
<!-- 标记当前导航项在列表中的位置从1开始 -->
<meta itemprop="position" content="{{ forloop.counter }}"/>
<!-- 元数据当前项在面包屑中的位置序号forloop.counter获取循环次数 -->
</li>
{% endfor %}
{% endfor %} <!-- 导航项循环结束 -->
<!-- 面包屑最后一级(当前页面),通常显示为激活状态 -->
<!-- 面包屑最后一级(当前页面),添加active类表示激活状态 -->
<li class="active" itemprop="itemListElement" itemscope
itemtype="https://schema.org/ListItem">
<span itemprop="name">{{ title }}</span>
<!-- 当前页面标题,无链接 -->
<span itemprop="name">{{ title }}</span> <!-- 当前页面名称,无链接 -->
<!-- 标记最后一级在列表中的位置由count变量指定 -->
<meta itemprop="position" content="{{ count }}"/>
<!-- 元数据:当前项在面包屑中的位置(总计数) -->
</li>
</ul>
</ul> <!-- 面包屑导航列表结束 -->

@ -0,0 +1,86 @@
import logging
from django.utils import timezone
from djangoblog.utils import cache, get_blog_setting
from .models import Category, Article
logger = logging.getLogger(__name__)
def seo_processor(requests):
"""
SEO上下文处理器
这个函数是一个Django上下文处理器用于向所有模板传递SEO相关的变量
它使用缓存来提高性能避免每次请求都查询数据库
Args:
requests: Django请求对象包含当前请求的信息
Returns:
dict: 包含SEO和网站设置信息的字典这些变量将在所有模板中可用
"""
# 缓存键名
key = 'seo_processor'
# 尝试从缓存中获取数据
value = cache.get(key)
if value:
# 如果缓存存在,直接返回缓存数据
return value
else:
# 缓存不存在,重新生成数据
logger.info('set processor cache.')
# 获取博客全局设置
setting = get_blog_setting()
# 构建包含所有SEO和网站设置信息的字典
value = {
# 网站基本信息
'SITE_NAME': setting.site_name, # 网站名称
'SITE_SEO_DESCRIPTION': setting.site_seo_description, # SEO描述
'SITE_DESCRIPTION': setting.site_description, # 网站描述
'SITE_KEYWORDS': setting.site_keywords, # 网站关键词
# 网站URL相关
'SITE_BASE_URL': requests.scheme + '://' + requests.get_host() + '/', # 网站基础URL
# 文章相关设置
'ARTICLE_SUB_LENGTH': setting.article_sub_length, # 文章摘要长度
# 导航数据
'nav_category_list': Category.objects.all(), # 所有分类(用于导航菜单)
'nav_pages': Article.objects.filter(
type='p', # 页面类型
status='p'), # 已发布状态
# 评论系统设置
'OPEN_SITE_COMMENT': setting.open_site_comment, # 是否开启评论
'COMMENT_NEED_REVIEW': setting.comment_need_review, # 评论是否需要审核
# 备案信息
'BEIAN_CODE': setting.beian_code, # ICP备案号
"BEIAN_CODE_GONGAN": setting.gongan_beiancode, # 公安备案号
"SHOW_GONGAN_CODE": setting.show_gongan_code, # 是否显示公安备案
# 广告相关
'SHOW_GOOGLE_ADSENSE': setting.show_google_adsense, # 是否显示Google广告
'GOOGLE_ADSENSE_CODES': setting.google_adsense_codes, # Google广告代码
# 统计代码
'ANALYTICS_CODE': setting.analytics_code, # 网站统计代码(如百度统计)
# 时间信息
"CURRENT_YEAR": timezone.now().year, # 当前年份
# 全局页头页脚
"GLOBAL_HEADER": setting.global_header, # 全局头部HTML
"GLOBAL_FOOTER": setting.global_footer, # 全局尾部HTML
}
# 将数据存入缓存有效期10小时60 * 60 * 10秒
cache.set(key, value, 60 * 60 * 10)
return value

@ -0,0 +1,283 @@
import time
import elasticsearch.client
from django.conf import settings
from elasticsearch_dsl import Document, InnerDoc, Date, Integer, Long, Text, Object, GeoPoint, Keyword, Boolean
from elasticsearch_dsl.connections import connections
from blog.models import Article
# 检查是否启用了Elasticsearch配置
ELASTICSEARCH_ENABLED = hasattr(settings, 'ELASTICSEARCH_DSL')
if ELASTICSEARCH_ENABLED:
# 创建Elasticsearch连接
connections.create_connection(
hosts=[settings.ELASTICSEARCH_DSL['default']['hosts']])
from elasticsearch import Elasticsearch
# 初始化Elasticsearch客户端
es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
from elasticsearch.client import IngestClient
# 创建Ingest管道客户端用于数据处理管道
c = IngestClient(es)
try:
# 检查是否已存在geoip管道
c.get_pipeline('geoip')
except elasticsearch.exceptions.NotFoundError:
# 如果不存在创建geoip管道用于IP地理位置解析
c.put_pipeline('geoip', body='''{
"description" : "Add geoip info",
"processors" : [
{
"geoip" : {
"field" : "ip"
}
}
]
}''')
class GeoIp(InnerDoc):
"""IP地理位置信息内嵌文档"""
continent_name = Keyword() # 大洲名称
country_iso_code = Keyword() # 国家ISO代码
country_name = Keyword() # 国家名称
location = GeoPoint() # 地理位置坐标
class UserAgentBrowser(InnerDoc):
"""用户代理浏览器信息"""
Family = Keyword() # 浏览器家族
Version = Keyword() # 浏览器版本
class UserAgentOS(UserAgentBrowser):
"""用户代理操作系统信息"""
pass # 继承自UserAgentBrowser具有相同的字段结构
class UserAgentDevice(InnerDoc):
"""用户代理设备信息"""
Family = Keyword() # 设备家族
Brand = Keyword() # 设备品牌
Model = Keyword() # 设备型号
class UserAgent(InnerDoc):
"""完整的用户代理信息"""
browser = Object(UserAgentBrowser, required=False) # 浏览器信息对象
os = Object(UserAgentOS, required=False) # 操作系统信息对象
device = Object(UserAgentDevice, required=False) # 设备信息对象
string = Text() # 原始用户代理字符串
is_bot = Boolean() # 是否为爬虫/机器人
class ElapsedTimeDocument(Document):
"""
性能监控文档 - 用于记录请求响应时间等性能数据
这个文档类型用于存储网站性能监控数据包括
- 请求URL和响应时间
- 用户IP和地理位置
- 用户代理信息
"""
url = Keyword() # 请求的URL
time_taken = Long() # 请求耗时(毫秒)
log_datetime = Date() # 日志时间
ip = Keyword() # 用户IP地址
geoip = Object(GeoIp, required=False) # IP地理位置信息
useragent = Object(UserAgent, required=False) # 用户代理信息
class Index:
"""索引配置"""
name = 'performance' # 索引名称
settings = {
"number_of_shards": 1, # 分片数量
"number_of_replicas": 0 # 副本数量
}
class Meta:
doc_type = 'ElapsedTime' # 文档类型
class ElaspedTimeDocumentManager:
"""性能监控文档管理器"""
@staticmethod
def build_index():
"""创建性能监控索引"""
from elasticsearch import Elasticsearch
client = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
res = client.indices.exists(index="performance")
if not res:
ElapsedTimeDocument.init() # 初始化索引映射
@staticmethod
def delete_index():
"""删除性能监控索引"""
from elasticsearch import Elasticsearch
es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
es.indices.delete(index='performance', ignore=[400, 404]) # 忽略404错误
@staticmethod
def create(url, time_taken, log_datetime, useragent, ip):
"""
创建性能监控记录
Args:
url: 请求URL
time_taken: 请求耗时
log_datetime: 日志时间
useragent: 用户代理对象
ip: 用户IP地址
"""
ElaspedTimeDocumentManager.build_index()
# 构建用户代理信息
ua = UserAgent()
ua.browser = UserAgentBrowser()
ua.browser.Family = useragent.browser.family # 浏览器家族
ua.browser.Version = useragent.browser.version_string # 浏览器版本
ua.os = UserAgentOS()
ua.os.Family = useragent.os.family # 操作系统家族
ua.os.Version = useragent.os.version_string # 操作系统版本
ua.device = UserAgentDevice()
ua.device.Family = useragent.device.family # 设备家族
ua.device.Brand = useragent.device.brand # 设备品牌
ua.device.Model = useragent.device.model # 设备型号
ua.string = useragent.ua_string # 原始UA字符串
ua.is_bot = useragent.is_bot # 是否为机器人
# 创建文档并使用geoip管道处理IP地理位置
doc = ElapsedTimeDocument(
meta={
'id': int(round(time.time() * 1000)) # 使用时间戳作为文档ID
},
url=url,
time_taken=time_taken,
log_datetime=log_datetime,
useragent=ua,
ip=ip
)
doc.save(pipeline="geoip") # 使用geoip管道自动添加地理位置信息
class ArticleDocument(Document):
"""
文章搜索文档 - 用于Elasticsearch全文搜索
这个文档类型定义了文章在Elasticsearch中的索引结构
支持对文章标题内容作者分类标签等进行全文搜索
"""
body = Text(analyzer='ik_max_word', search_analyzer='ik_smart') # 文章内容使用IK中文分词器
title = Text(analyzer='ik_max_word', search_analyzer='ik_smart') # 文章标题使用IK中文分词器
author = Object(properties={
'nickname': Text(analyzer='ik_max_word', search_analyzer='ik_smart'), # 作者昵称
'id': Integer() # 作者ID
})
category = Object(properties={
'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'), # 分类名称
'id': Integer() # 分类ID
})
tags = Object(properties={
'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'), # 标签名称
'id': Integer() # 标签ID
})
# 文章元数据字段
pub_time = Date() # 发布时间
status = Text() # 文章状态(发布/草稿)
comment_status = Text() # 评论状态(开启/关闭)
type = Text() # 文章类型(文章/页面)
views = Integer() # 浏览次数
article_order = Integer() # 文章排序
class Index:
"""索引配置"""
name = 'blog' # 索引名称
settings = {
"number_of_shards": 1, # 分片数量
"number_of_replicas": 0 # 副本数量
}
class Meta:
doc_type = 'Article' # 文档类型
class ArticleDocumentManager():
"""文章文档管理器 - 负责文章搜索索引的创建、更新和管理"""
def __init__(self):
self.create_index()
def create_index(self):
"""创建文章搜索索引"""
ArticleDocument.init()
def delete_index(self):
"""删除文章搜索索引"""
from elasticsearch import Elasticsearch
es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
es.indices.delete(index='blog', ignore=[400, 404]) # 忽略404错误
def convert_to_doc(self, articles):
"""
将Django文章对象转换为Elasticsearch文档对象
Args:
articles: Django文章查询集
Returns:
list: Elasticsearch文档对象列表
"""
return [
ArticleDocument(
meta={'id': article.id}, # 使用文章ID作为文档ID
body=article.body,
title=article.title,
author={
'nickname': article.author.username,
'id': article.author.id
},
category={
'name': article.category.name,
'id': article.category.id
},
tags=[
{'name': t.name, 'id': t.id} for t in article.tags.all() # 转换标签列表
],
pub_time=article.pub_time,
status=article.status,
comment_status=article.comment_status,
type=article.type,
views=article.views,
article_order=article.article_order
) for article in articles
]
def rebuild(self, articles=None):
"""
重建文章搜索索引
Args:
articles: 要索引的文章列表如果为None则索引所有文章
"""
ArticleDocument.init() # 重新初始化索引
articles = articles if articles else Article.objects.all() # 获取所有文章或指定文章
docs = self.convert_to_doc(articles) # 转换为文档对象
for doc in docs:
doc.save() # 保存到Elasticsearch
def update_docs(self, docs):
"""
更新文档索引
Args:
docs: 要更新的文档列表
"""
for doc in docs:
doc.save() # 保存更新到Elasticsearch

@ -0,0 +1,316 @@
# 导入必要模块
from django.utils.encoding import force_str # 用于将数据转换为字符串兼容Python 2/3
from elasticsearch_dsl import Q # Elasticsearch DSL的查询构建工具
from haystack.backends import ( # Haystack搜索框架的基础类
BaseEngine, BaseSearchBackend, BaseSearchQuery, log_query
)
from haystack.forms import ModelSearchForm # Haystack默认的模型搜索表单
from haystack.models import SearchResult # Haystack的搜索结果封装类
from haystack.utils import log as logging # Haystack的日志工具
# 导入项目内部模块
from blog.documents import ArticleDocument, ArticleDocumentManager # 文章的Elasticsearch文档定义及管理器
from blog.models import Article # 博客文章模型
# 创建当前模块的日志记录器,用于记录搜索相关日志
logger = logging.getLogger(__name__)
class ElasticSearchBackend(BaseSearchBackend):
"""
基于Elasticsearch的搜索后端实现继承自Haystack的BaseSearchBackend
作用实现与Elasticsearch的交互逻辑包括索引的创建更新删除
以及搜索查询的执行拼写建议等功能
"""
def __init__(self, connection_alias, **connection_options):
"""
初始化搜索后端
:param connection_alias: 数据库连接别名用于多后端配置
:param connection_options: 连接参数如主机端口等
"""
super(ElasticSearchBackend, self).__init__(connection_alias,** connection_options)
self.manager = ArticleDocumentManager() # 初始化文章文档管理器(处理索引操作)
self.include_spelling = True # 启用拼写建议功能
def _get_models(self, iterable):
"""
将模型实例列表转换为Elasticsearch文档对象
:param iterable: 模型实例列表如Article对象列表
:return: 转换后的Elasticsearch文档列表
"""
# 若输入为空,默认使用所有已发布的文章
models = iterable if iterable and iterable[0] else Article.objects.all()
# 通过管理器将模型转换为文档
docs = self.manager.convert_to_doc(models)
return docs
def _create(self, models):
"""
创建索引并初始化文档全量重建索引时使用
:param models: 模型实例列表
"""
self.manager.create_index() # 创建Elasticsearch索引若不存在
docs = self._get_models(models) # 转换模型为文档
self.manager.rebuild(docs) # 全量重建索引(清空旧数据后插入新数据)
def _delete(self, models):
"""
从索引中删除指定模型对应的文档
:param models: 要删除的模型实例列表
:return: 操作是否成功始终返回True
"""
for m in models:
m.delete() # 调用文档的删除方法
return True
def _rebuild(self, models):
"""
增量更新索引适用于部分数据更新
:param models: 需要更新的模型实例列表若为空则更新所有文章
"""
models = models if models else Article.objects.all() # 处理空输入
docs = self._get_models(models) # 转换模型为文档
self.manager.update_docs(docs) # 增量更新文档
def update(self, index, iterable, commit=True):
"""
Haystack标准接口更新索引用于实时同步模型变更
:param index: 索引名称当前实现未使用由管理器处理
:param iterable: 模型实例列表
:param commit: 是否立即提交当前实现未使用
"""
models = self._get_models(iterable) # 转换模型为文档
self.manager.update_docs(models) # 执行更新
def remove(self, obj_or_string):
"""
Haystack标准接口从索引中移除指定对象
:param obj_or_string: 模型实例或ID字符串
"""
models = self._get_models([obj_or_string]) # 转换为文档
self._delete(models) # 执行删除
def clear(self, models=None, commit=True):
"""
Haystack标准接口清空索引或指定模型的索引
:param models: 可选指定要清空的模型类当前实现忽略清空所有
:param commit: 是否立即提交当前实现未使用
"""
self.remove(None) # 调用删除方法清空所有
@staticmethod
def get_suggestion(query: str) -> str:
"""
获取搜索建议词基于Elasticsearch的拼写纠错功能
:param query: 用户输入的搜索词
:return: 建议的修正词多个词用空格拼接
"""
# 构建搜索查询:匹配文章内容,并启用拼写建议
search = ArticleDocument.search() \
.query("match", body=query) \
.suggest('suggest_search', query, term={'field': 'body'}) \
.execute()
keywords = []
# 提取建议结果
for suggest in search.suggest.suggest_search:
if suggest["options"]: # 若有建议词,取第一个
keywords.append(suggest["options"][0]["text"])
else: # 若无建议,保留原词
keywords.append(suggest["text"])
return ' '.join(keywords) # 拼接建议词为字符串
@log_query # Haystack装饰器记录查询日志
def search(self, query_string, **kwargs):
"""
执行搜索查询的核心方法
:param query_string: 用户输入的搜索字符串
:param kwargs: 额外参数如分页偏移量start_offset/end_offset
:return: 搜索结果字典包含结果列表命中数拼写建议等
"""
logger.info('search query_string:' + query_string) # 记录搜索词
# 获取分页参数(用于限制返回结果范围)
start_offset = kwargs.get('start_offset', 0)
end_offset = kwargs.get('end_offset', 10) # 默认返回前10条
# 判断是否需要启用拼写建议通过is_suggest参数控制
if getattr(self, "is_suggest", None):
suggestion = self.get_suggestion(query_string) # 获取建议词
else:
suggestion = query_string # 不启用建议,使用原搜索词
# 构建Elasticsearch查询条件
# 1. 布尔查询should匹配标题或内容至少满足70%的条件
q = Q('bool',
should=[Q('match', body=suggestion), Q('match', title=suggestion)],
minimum_should_match="70%")
# 构建完整搜索:
# - 应用上述查询条件
# - 过滤仅包含已发布status='p'的文章type='a'
# - 不返回原始文档内容source=False
# - 应用分页偏移
search = ArticleDocument.search() \
.query('bool', filter=[q]) \
.filter('term', status='p') \
.filter('term', type='a') \
.source(False)[start_offset: end_offset]
# 执行搜索并处理结果
results = search.execute()
hits = results['hits'].total # 总命中数
raw_results = []
# 遍历搜索结果转换为Haystack的SearchResult格式
for raw_result in results['hits']['hits']:
app_label = 'blog' # 应用标签(固定为博客应用)
model_name = 'Article' # 模型名称(固定为文章模型)
additional_fields = {} # 额外字段(当前未使用)
# 创建SearchResult实例适配Haystack的结果格式
result = SearchResult(
app_label,
model_name,
raw_result['_id'], # 文档ID对应文章ID
raw_result['_score'], # 匹配得分
**additional_fields
)
raw_results.append(result)
# 构建返回结果字典
facets = {} # 分面搜索结果(当前未实现)
# 若建议词与原词不同则返回建议词否则为None
spelling_suggestion = None if query_string == suggestion else suggestion
return {
'results': raw_results, # 搜索结果列表SearchResult实例
'hits': hits, # 总命中数
'facets': facets, # 分面数据
'spelling_suggestion': spelling_suggestion, # 拼写建议
}
class ElasticSearchQuery(BaseSearchQuery):
"""
自定义搜索查询类继承自Haystack的BaseSearchQuery
作用处理搜索查询的构建逻辑包括查询字符串清洗参数转换等
"""
def _convert_datetime(self, date):
"""
转换日期时间为Elasticsearch兼容的字符串格式
:param date: 日期时间对象
:return: 格式化的字符串'20231018123000'
"""
if hasattr(date, 'hour'): # 若包含时间信息datetime对象
return force_str(date.strftime('%Y%m%d%H%M%S'))
else: # 仅日期date对象时间部分设为00:00:00
return force_str(date.strftime('%Y%m%d000000'))
def clean(self, query_fragment):
"""
清洗用户输入的查询片段处理保留字和特殊字符
:param query_fragment: 用户输入的查询字符串片段
:return: 清洗后的查询字符串
"""
words = query_fragment.split() # 按空格拆分词语
cleaned_words = []
for word in words:
# 处理Elasticsearch保留字转为小写
if word in self.backend.RESERVED_WORDS:
word = word.replace(word, word.lower())
# 处理特殊字符(若包含特殊字符,用单引号包裹)
for char in self.backend.RESERVED_CHARACTERS:
if char in word:
word = "'%s'" % word
break
cleaned_words.append(word)
return ' '.join(cleaned_words) # 拼接清洗后的词语
def build_query_fragment(self, field, filter_type, value):
"""
构建查询片段适配Elasticsearch的查询语法
:param field: 搜索字段
:param filter_type: 过滤类型
:param value: 查询值
:return: 构建的查询字符串
"""
return value.query_string # 直接使用查询字符串由value提供
def get_count(self):
"""
获取搜索结果总数
:return: 结果数量
"""
results = self.get_results()
return len(results) if results else 0
def get_spelling_suggestion(self, preferred_query=None):
"""
获取拼写建议适配Haystack接口
:param preferred_query: 优先使用的查询未使用
:return: 拼写建议词
"""
return self._spelling_suggestion
def build_params(self, spelling_query=None):
"""
构建查询参数适配Haystack接口
:param spelling_query: 拼写建议查询未使用
:return: 构建的参数字典
"""
kwargs = super(ElasticSearchQuery, self).build_params(spelling_query=spelling_query)
return kwargs
class ElasticSearchModelSearchForm(ModelSearchForm):
"""
自定义搜索表单继承自Haystack的ModelSearchForm
作用扩展默认搜索表单支持控制是否启用拼写建议
"""
def search(self):
"""
执行搜索根据表单参数控制拼写建议
:return: 搜索结果集SearchQuerySet
"""
# 通过表单数据中的"is_suggest"参数控制是否启用拼写建议
# 若"is_suggest"为"no",则禁用建议
self.searchqueryset.query.backend.is_suggest = self.data.get("is_suggest") != "no"
# 调用父类方法执行搜索
sqs = super().search()
return sqs
class ElasticSearchEngine(BaseEngine):
"""
Elasticsearch搜索引擎入口类继承自Haystack的BaseEngine
作用绑定后端实现和查询类作为Haystack的引擎配置入口
"""
backend = ElasticSearchBackend # 指定使用的搜索后端
query = ElasticSearchQuery # 指定使用的查询类

@ -0,0 +1,83 @@
# 导入必要的模块和类
from django.contrib.auth import get_user_model # 用于获取自定义用户模型
from django.contrib.syndication.views import Feed # Django内置的Feed基类用于生成RSS/Atom订阅
from django.utils import timezone # 处理时间相关操作
from django.utils.feedgenerator import Rss201rev2Feed # RSS 2.0版本的生成器
from blog.models import Article # 导入博客文章模型
from djangoblog.utils import CommonMarkdown # 导入Markdown处理工具用于将文章内容转换为HTML
class DjangoBlogFeed(Feed):
"""
自定义博客RSS订阅Feed类继承自Django的Feed基类用于生成博客文章的RSS订阅源
"""
# 指定Feed类型为RSS 2.0版本符合Rss201rev2Feed规范
feed_type = Rss201rev2Feed
# RSS源的描述信息会显示在订阅源的描述中
description = '大巧无工,重剑无锋.'
# RSS源的标题订阅源的名称
title = "且听风吟 大巧无工,重剑无锋. "
# RSS源的链接通常指向网站的订阅页面
link = "/feed/"
def author_name(self):
"""
定义订阅源的作者名称
这里取系统中第一个用户的昵称作为作者名
"""
return get_user_model().objects.first().nickname
def author_link(self):
"""
定义订阅源作者的链接
这里取系统中第一个用户的个人主页链接需用户模型实现get_absolute_url方法
"""
return get_user_model().objects.first().get_absolute_url()
def items(self):
"""
定义订阅源包含的项目即文章列表
返回条件类型为'article'type='a'状态为'已发布'status='p'的文章
排序方式按发布时间倒序最新发布的在前
数量限制最多返回5篇文章
"""
return Article.objects.filter(type='a', status='p').order_by('-pub_time')[:5]
def item_title(self, item):
"""
定义单个项目文章的标题
参数item从items()方法返回的单个Article对象
返回文章的标题
"""
return item.title
def item_description(self, item):
"""
定义单个项目文章的描述内容
使用CommonMarkdown工具将文章的Markdown格式正文转换为HTML作为订阅中的描述
"""
return CommonMarkdown.get_markdown(item.body)
def feed_copyright(self):
"""
定义订阅源的版权信息
格式为"Copyright© 年份 且听风吟"年份自动获取当前时间的年份
"""
now = timezone.now()
return "Copyright© {year} 且听风吟".format(year=now.year)
def item_link(self, item):
"""
定义单个项目文章的链接
返回文章的绝对URL需Article模型实现get_absolute_url方法
"""
return item.get_absolute_url()
def item_guid(self, item):
"""
定义单个项目的全局唯一标识符GUID
此处未实现具体逻辑可根据需求补充如返回文章ID或唯一URL等
"""
return

@ -0,0 +1,186 @@
<<<<<<< HEAD
from django import forms
<<<<<<< HEAD
from django.contrib.auth import get_user_model, password_validation
from django.contrib.auth.forms import AuthenticationForm, UserCreationForm
from django.core.exceptions import ValidationError
from django.forms import widgets
from django.utils.translation import gettext_lazy as _
from . import utils
from .models import BlogUser
class LoginForm(AuthenticationForm):
def __init__(self, *args, **kwargs):
super(LoginForm, self).__init__(*args, **kwargs)
self.fields['username'].widget = widgets.TextInput(
attrs={'placeholder': "username", "class": "form-control"})
self.fields['password'].widget = widgets.PasswordInput(
attrs={'placeholder': "password", "class": "form-control"})
class RegisterForm(UserCreationForm):
def __init__(self, *args, **kwargs):
super(RegisterForm, self).__init__(*args, **kwargs)
self.fields['username'].widget = widgets.TextInput(
attrs={'placeholder': "username", "class": "form-control"})
self.fields['email'].widget = widgets.EmailInput(
attrs={'placeholder': "email", "class": "form-control"})
self.fields['password1'].widget = widgets.PasswordInput(
attrs={'placeholder': "password", "class": "form-control"})
self.fields['password2'].widget = widgets.PasswordInput(
attrs={'placeholder': "repeat password", "class": "form-control"})
def clean_email(self):
email = self.cleaned_data['email']
if get_user_model().objects.filter(email=email).exists():
raise ValidationError(_("email already exists"))
return email
class Meta:
model = get_user_model()
fields = ("username", "email")
class ForgetPasswordForm(forms.Form):
new_password1 = forms.CharField(
label=_("New password"),
widget=forms.PasswordInput(
attrs={
"class": "form-control",
'placeholder': _("New password")
}
),
)
new_password2 = forms.CharField(
label="确认密码",
widget=forms.PasswordInput(
attrs={
"class": "form-control",
'placeholder': _("Confirm password")
}
),
)
email = forms.EmailField(
label='邮箱',
widget=forms.TextInput(
attrs={
'class': 'form-control',
'placeholder': _("Email")
}
),
)
code = forms.CharField(
label=_('Code'),
widget=forms.TextInput(
attrs={
'class': 'form-control',
'placeholder': _("Code")
}
),
)
def clean_new_password2(self):
password1 = self.data.get("new_password1")
password2 = self.data.get("new_password2")
if password1 and password2 and password1 != password2:
raise ValidationError(_("passwords do not match"))
password_validation.validate_password(password2)
return password2
def clean_email(self):
user_email = self.cleaned_data.get("email")
if not BlogUser.objects.filter(
email=user_email
).exists():
# todo 这里的报错提示可以判断一个邮箱是不是注册过,如果不想暴露可以修改
raise ValidationError(_("email does not exist"))
return user_email
def clean_code(self):
code = self.cleaned_data.get("code")
error = utils.verify(
email=self.cleaned_data.get("email"),
code=code,
)
if error:
raise ValidationError(error)
return code
class ForgetPasswordCodeForm(forms.Form):
email = forms.EmailField(
label=_('Email'),
)
=======
from django.forms import ModelForm
from .models import Comment
class CommentForm(ModelForm): # 定义评论表单类继承自ModelForm
# 添加父评论ID字段用于实现评论回复功能
# 使用HiddenInput控件隐藏显示且非必填顶级评论无需父ID
parent_comment_id = forms.IntegerField(
widget=forms.HiddenInput, required=False)
class Meta: # Meta类用于配置表单与模型的关联信息
model = Comment # 指定表单对应的模型为Comment
fields = ['body'] # 表单需要包含的模型字段这里只包含评论内容body
>>>>>>> zh_branch
=======
import logging
from django import forms
from haystack.forms import SearchForm
logger = logging.getLogger(__name__)
class BlogSearchForm(SearchForm):
"""
博客搜索表单类
继承自Haystack的SearchForm用于处理博客文章的搜索功能
这个表单定义了搜索框的验证规则和搜索逻辑
"""
# 搜索查询字段,设置为必填字段
querydata = forms.CharField(required=True)
def search(self):
"""
执行搜索操作
重写父类的search方法添加自定义搜索逻辑
1. 调用父类的搜索方法获取基础搜索结果
2. 验证表单数据是否有效
3. 记录搜索关键词到日志
4. 返回搜索结果
Returns:
SearchQuerySet: 搜索结果的查询集
Raises:
如果表单无效返回空搜索结果
"""
# 调用父类的search方法获取基础搜索结果
datas = super(BlogSearchForm, self).search()
# 检查表单数据是否有效
if not self.is_valid():
# 如果表单无效,返回空搜索结果
return self.no_query_found()
# 如果搜索关键词存在,记录到日志中(用于搜索统计和分析)
if self.cleaned_data['querydata']:
logger.info(self.cleaned_data['querydata'])
# 返回搜索结果
return datas
>>>>>>> hyt_branch

@ -0,0 +1,30 @@
# 文章相关系统事件常量定义
# 用途:统一管理插件系统中与文章操作相关的事件名称,避免硬编码导致的不一致问题
# 所有事件名称均采用大写蛇形命名法UPPER_SNAKE_CASE符合Python常量命名规范
# 事件:文章详情页加载完成
# 触发时机:当用户访问某篇文章的详情页,页面内容加载完成后触发
# 应用场景:插件可监听此事件,执行详情页相关的自定义逻辑(如添加页面统计代码、注入额外内容等)
ARTICLE_DETAIL_LOAD = 'article_detail_load'
# 事件:文章创建完成
# 触发时机:当一篇新文章在系统中创建成功(如数据库写入完成、状态设为"已发布"或"草稿")后触发
# 应用场景:插件可监听此事件,执行创建后的后续操作(如自动生成文章摘要、同步到外部平台、发送通知等)
ARTICLE_CREATE = 'article_create'
# 事件:文章更新完成
# 触发时机:当已存在的文章内容、属性(如标题、分类、状态)修改并保存成功后触发
# 应用场景:插件可监听此事件,执行更新后的联动操作(如更新文章索引、记录修改日志、重新生成相关统计数据等)
ARTICLE_UPDATE = 'article_update'
# 事件:文章删除完成
# 触发时机:当一篇文章从系统中删除(物理删除或逻辑删除,如标记为"已删除"状态)后触发
# 应用场景:插件可监听此事件,执行删除后的清理操作(如删除关联的评论、移除相关缓存、同步删除外部存储的附件等)
ARTICLE_DELETE = 'article_delete'
# 文章内容钩子名称常量
# 用途:定义专门用于拦截、修改文章内容的钩子标识,与上述"操作事件"区分(事件侧重流程节点,钩子侧重内容处理)
# 命名格式与事件常量保持一致,确保插件系统中钩子名称的唯一性和可识别性
# 应用场景插件可注册此钩子在文章内容渲染前如详情页展示、导出为PDF对内容进行自定义处理如过滤敏感词、替换关键词、添加水印等
ARTICLE_CONTENT_HOOK_NAME = "the_content"

@ -0,0 +1,92 @@
# 导入logging模块用于记录钩子系统运行过程中的日志如注册信息、错误信息等
import logging
# 创建当前模块的日志记录器,日志名称与模块绑定,便于区分不同组件的日志输出
logger = logging.getLogger(__name__)
# 全局钩子存储字典,用于保存所有注册的钩子及其对应的回调函数
# 键:钩子名称(字符串,如"article_create"
# 值:回调函数列表(所有注册到该钩子的可调用对象将按注册顺序存储)
_hooks = {}
def register(hook_name: str, callback: callable):
"""
注册一个钩子回调函数将其添加到指定钩子名称对应的回调列表中
核心作用建立"钩子名称""处理逻辑(回调函数)"的映射关系
使后续触发钩子时能自动执行所有注册的回调
:param hook_name: 钩子名称字符串需与触发时使用的名称一致如ARTICLE_CREATE
:param callback: 可调用对象函数方法等当钩子被触发时会执行此对象
回调函数的参数需与钩子触发时传递的参数匹配
"""
# 若钩子名称尚未在全局字典中,初始化一个空列表用于存储回调
if hook_name not in _hooks:
_hooks[hook_name] = []
# 将回调函数添加到对应钩子的列表中(按注册顺序存储,触发时也按此顺序执行)
_hooks[hook_name].append(callback)
# 记录DEBUG级日志说明钩子注册成功包含钩子名称和回调函数名便于调试
logger.debug(f"Registered hook '{hook_name}' with callback '{callback.__name__}'")
def run_action(hook_name: str, *args, **kwargs):
"""
执行指定名称的"动作钩子Action Hook"按注册顺序调用所有关联的回调函数
动作钩子特性用于触发一系列操作不关注返回值仅执行回调逻辑
典型场景文章创建后发送通知记录日志等执行动作但无需修改数据
:param hook_name: 要触发的钩子名称需已被注册过
:param *args: 传递给回调函数的位置参数可变参数根据钩子场景定义
:param **kwargs: 传递给回调函数的关键字参数可变参数根据钩子场景定义
"""
# 检查该钩子是否有已注册的回调函数
if hook_name in _hooks:
# 记录DEBUG级日志说明开始执行该动作钩子
logger.debug(f"Running action hook '{hook_name}'")
# 按注册顺序遍历所有回调函数并执行
for callback in _hooks[hook_name]:
try:
# 传递位置参数和关键字参数给回调函数
callback(*args, **kwargs)
except Exception as e:
# 若回调执行出错记录ERROR级日志包含详细异常信息
# exc_info=True 会在日志中附带堆栈跟踪,便于排查错误
logger.error(
f"Error running action hook '{hook_name}' callback '{callback.__name__}': {e}",
exc_info=True
)
def apply_filters(hook_name: str, value, *args, **kwargs):
"""
执行指定名称的"过滤钩子Filter Hook"通过回调函数链式处理初始值并返回最终结果
过滤钩子特性用于对数据进行加工处理每个回调函数接收上一个函数的输出作为输入
最终返回经过所有回调处理后的值
典型场景文章内容过滤敏感词格式化文本等修改数据并返回新值
:param hook_name: 要触发的钩子名称需已被注册过
:param value: 初始值需要被过滤/处理的数据如文章内容字符串
:param *args: 传递给回调函数的额外位置参数
:param **kwargs: 传递给回调函数的额外关键字参数
:return: 经过所有回调函数处理后的最终值
"""
# 检查该钩子是否有已注册的回调函数
if hook_name in _hooks:
# 记录DEBUG级日志说明开始执行该过滤钩子
logger.debug(f"Applying filter hook '{hook_name}'")
# 按注册顺序遍历所有回调函数,链式处理初始值
for callback in _hooks[hook_name]:
try:
# 调用回调函数,将当前值和额外参数传入,更新值为回调返回的结果
value = callback(value, *args, **kwargs)
except Exception as e:
# 若回调执行出错记录ERROR级日志包含详细异常信息
logger.error(
f"Error applying filter hook '{hook_name}' callback '{callback.__name__}': {e}",
exc_info=True
)
# 返回经过所有过滤处理后的最终值
return value

@ -0,0 +1,51 @@
# 导入必要的模块
# os: 用于处理文件路径和目录操作
# logging: 用于记录插件加载过程中的日志信息(成功/失败状态)
# django.conf.settings: 用于获取Django项目的配置信息如插件目录、激活的插件列表
import os
import logging
from django.conf import settings
# 创建当前模块的日志记录器,日志名称与模块绑定,便于追踪插件加载相关的日志
logger = logging.getLogger(__name__)
def load_plugins():
"""
动态加载并初始化位于'plugins'目录中的插件
功能说明
- 从Django配置中读取激活的插件列表settings.ACTIVE_PLUGINS
- 检查每个插件的目录结构是否合法是否存在plugin.py文件
- 动态导入插件的核心模块plugin.py触发插件的初始化流程
- 通过日志记录每个插件的加载结果成功/失败及原因
调用时机
该函数应在Django应用注册表app registry准备就绪后调用
通常在项目启动时如通过AppConfig.ready()方法触发确保Django环境已初始化完成
"""
# 遍历配置中激活的所有插件名称settings.ACTIVE_PLUGINS是一个插件名称列表
for plugin_name in settings.ACTIVE_PLUGINS:
# 构建插件的完整目录路径:
# settings.PLUGINS_DIR是项目中存放所有插件的根目录如"project_root/plugins"
# 拼接根目录与当前插件名称,得到具体插件的目录路径(如"project_root/plugins/my_plugin"
plugin_path = os.path.join(settings.PLUGINS_DIR, plugin_name)
# 验证插件目录的合法性:
# 1. 必须是一个存在的目录os.path.isdir(plugin_path)
# 2. 目录中必须包含核心文件plugin.py插件的入口模块
if os.path.isdir(plugin_path) and os.path.exists(os.path.join(plugin_path, 'plugin.py')):
try:
# 动态导入插件的plugin.py模块
# 导入路径格式为"plugins.{插件名称}.plugin"基于Python包结构
# 导入后会自动执行plugin.py中的顶级代码如插件类的定义和注册逻辑
__import__(f'plugins.{plugin_name}.plugin')
# 记录INFO级日志提示插件加载成功
logger.info(f"Successfully loaded plugin: {plugin_name}")
# 捕获导入过程中的异常(如模块不存在、语法错误、依赖缺失等)
except ImportError as e:
# 记录ERROR级日志提示插件导入失败并附带异常信息
# exc_info=e 会将异常堆栈信息写入日志,便于排查问题
logger.error(f"Failed to import plugin: {plugin_name}", exc_info=e)

@ -0,0 +1,145 @@
# 导入Django管理后台核心模块
from django.contrib import admin
# 导入日志相关常量和模型DELETION表示删除操作的标记
from django.contrib.admin.models import DELETION
# 导入ContentType模型用于处理模型与数据库表的映射关系
from django.contrib.contenttypes.models import ContentType
# 导入URL反向解析和异常处理
from django.urls import reverse, NoReverseMatch
# 导入字符串处理工具
from django.utils.encoding import force_str
# 导入HTML转义工具防止XSS攻击
from django.utils.html import escape
# 导入安全字符串标记工具标记可信HTML
from django.utils.safestring import mark_safe
# 导入国际化翻译工具
from django.utils.translation import gettext_lazy as _
class LogEntryAdmin(admin.ModelAdmin):
"""
自定义日志条目(LogEntry)的管理后台配置类
用于在Django admin中展示和管理系统操作日志
"""
# 列表页的筛选器:按内容类型(关联的模型)筛选日志
list_filter = [
'content_type'
]
# 搜索字段:支持按对象名称和操作消息搜索日志
search_fields = [
'object_repr', # 对象的字符串表示
'change_message' # 操作描述消息
]
# 列表页中可点击的链接字段
list_display_links = [
'action_time', # 操作时间
'get_change_message', # 操作消息
]
# 列表页展示的字段
list_display = [
'action_time', # 操作时间
'user_link', # 操作用户(带链接)
'content_type', # 关联的模型类型
'object_link', # 操作的对象(带链接)
'get_change_message', # 操作消息
]
def has_add_permission(self, request):
"""
禁用添加日志的权限日志由系统自动生成不允许手动添加
"""
return False
def has_change_permission(self, request, obj=None):
"""
限制修改日志的权限
- 仅超级用户或拥有'admin.change_logentry'权限的用户可查看
- 禁止POST请求(防止修改操作)
"""
return (
request.user.is_superuser or
request.user.has_perm('admin.change_logentry')
) and request.method != 'POST'
def has_delete_permission(self, request, obj=None):
"""
禁用删除日志的权限日志需保留不允许删除
"""
return False
def object_link(self, obj):
"""
生成操作对象的链接
- 若操作不是删除且存在关联模型尝试生成指向该对象编辑页的链接
- 否则显示对象的字符串表示
"""
object_link = escape(obj.object_repr) # 转义对象名称防止XSS
content_type = obj.content_type
# 非删除操作且存在内容类型时尝试生成链接
if obj.action_flag != DELETION and content_type is not None:
try:
# 反向解析对象的admin编辑页URL
url = reverse(
'admin:{}_{}_change'.format(content_type.app_label,
content_type.model),
args=[obj.object_id]
)
# 生成带链接的HTML
object_link = '<a href="{}">{}</a>'.format(url, object_link)
except NoReverseMatch:
# 解析URL失败时仅显示对象名称
pass
# 标记为安全HTML避免被转义
return mark_safe(object_link)
# 配置列表页字段的排序和显示名称
object_link.admin_order_field = 'object_repr' # 允许按对象名称排序
object_link.short_description = _('object') # 字段显示名称(支持国际化)
def user_link(self, obj):
"""
生成操作用户的链接
- 尝试生成指向该用户编辑页的链接
- 否则显示用户的字符串表示
"""
# 获取用户模型对应的ContentType
content_type = ContentType.objects.get_for_model(type(obj.user))
user_link = escape(force_str(obj.user)) # 转义用户名
try:
# 反向解析用户的admin编辑页URL
url = reverse(
'admin:{}_{}_change'.format(content_type.app_label,
content_type.model),
args=[obj.user.pk]
)
# 生成带链接的HTML
user_link = '<a href="{}">{}</a>'.format(url, user_link)
except NoReverseMatch:
# 解析URL失败时仅显示用户名
pass
return mark_safe(user_link)
# 配置列表页字段的排序和显示名称
user_link.admin_order_field = 'user' # 允许按用户排序
user_link.short_description = _('user') # 字段显示名称(支持国际化)
def get_queryset(self, request):
"""
优化查询集预加载content_type关联数据减少数据库查询次数
"""
queryset = super(LogEntryAdmin, self).get_queryset(request)
return queryset.prefetch_related('content_type')
def get_actions(self, request):
"""
移除批量删除操作日志不允许批量删除
"""
actions = super(LogEntryAdmin, self).get_actions(request)
if 'delete_selected' in actions:
del actions['delete_selected']
return actions

@ -0,0 +1,104 @@
import logging
import time
from ipware import get_client_ip
from user_agents import parse
from blog.documents import ELASTICSEARCH_ENABLED, ElaspedTimeDocumentManager
logger = logging.getLogger(__name__)
class OnlineMiddleware(object):
"""
在线性能监控中间件
这个中间件用于监控网站的性能指标包括
- 页面渲染时间
- 用户访问信息
- 用户代理分析
- IP地理位置通过Elasticsearch geoip管道
继承自object是Django中间件的标准写法
"""
def __init__(self, get_response=None):
"""
初始化中间件
Args:
get_response: Django的下一个中间件或视图函数
"""
self.get_response = get_response
super().__init__()
def __call__(self, request):
"""
中间件主处理逻辑
这个方在每次请求时被调用用于
1. 记录请求开始时间
2. 执行后续中间件和视图
3. 计算页面渲染时间
4. 收集用户访问数据
5. 将数据存储到Elasticsearch如果启用
6. 在响应内容中插入加载时间
Args:
request: Django请求对象
Returns:
HttpResponse: 处理后的响应对象
"""
# 记录请求开始时间,用于计算页面渲染时间
start_time = time.time()
# 调用后续中间件和视图函数,获取响应
response = self.get_response(request)
# 从请求头中获取用户代理字符串
http_user_agent = request.META.get('HTTP_USER_AGENT', '')
# 获取客户端IP地址使用ipware库处理代理情况
ip, _ = get_client_ip(request)
# 解析用户代理字符串,获取浏览器、设备等信息
user_agent = parse(http_user_agent)
# 只处理非流式响应(避免对大文件下载等操作进行监控)
if not response.streaming:
try:
# 计算页面渲染总时间(秒)
cast_time = time.time() - start_time
# 如果启用了Elasticsearch记录性能数据
if ELASTICSEARCH_ENABLED:
# 将时间转换为毫秒并保留2位小数
time_taken = round((cast_time) * 1000, 2)
# 获取请求的URL路径
url = request.path
# 导入时区模块,获取当前时间
from django.utils import timezone
# 创建性能监控记录到Elasticsearch
ElaspedTimeDocumentManager.create(
url=url, # 请求URL
time_taken=time_taken, # 耗时(毫秒)
log_datetime=timezone.now(), # 记录时间
useragent=user_agent, # 用户代理信息
ip=ip # 客户端IP
)
# 在响应内容中替换加载时间占位符
# 将<!!LOAD_TIMES!!>替换为实际的加载时间取前5位
response.content = response.content.replace(
b'<!!LOAD_TIMES!!>', str.encode(str(cast_time)[:5]))
except Exception as e:
# 记录中间件执行过程中的任何错误
logger.error("Error OnlineMiddleware: %s" % e)
# 返回处理后的响应
return response

@ -0,0 +1,455 @@
<<<<<<< HEAD
<<<<<<< HEAD
from django.contrib.auth.models import AbstractUser
=======
import logging
import re
from abc import abstractmethod
from django.conf import settings
from django.core.exceptions import ValidationError
>>>>>>> hyt_branch
from django.db import models
from django.urls import reverse
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
<<<<<<< HEAD
from djangoblog.utils import get_current_site
# Create your models here.
class BlogUser(AbstractUser):
nickname = models.CharField(_('nick name'), max_length=100, blank=True)
creation_time = models.DateTimeField(_('creation time'), default=now)
last_modify_time = models.DateTimeField(_('last modify time'), default=now)
source = models.CharField(_('create source'), max_length=100, blank=True)
def get_absolute_url(self):
return reverse(
'blog:author_detail', kwargs={
'author_name': self.username})
def __str__(self):
return self.email
def get_full_url(self):
=======
from mdeditor.fields import MDTextField
from uuslug import slugify
from djangoblog.utils import cache_decorator, cache
from djangoblog.utils import get_current_site
logger = logging.getLogger(__name__)
class LinkShowType(models.TextChoices):
"""
链接显示类型选择
定义友情链接在网站中的显示位置
"""
I = ('i', _('index')) # 首页显示
L = ('l', _('list')) # 列表页显示
P = ('p', _('post')) # 文章页显示
A = ('a', _('all')) # 所有页面显示
S = ('s', _('slide')) # 幻灯片显示
class BaseModel(models.Model):
"""
基础模型类
所有模型的基类提供公共字段和方法
"""
id = models.AutoField(primary_key=True) # 自增主键
creation_time = models.DateTimeField(_('creation time'), default=now) # 创建时间
last_modify_time = models.DateTimeField(_('modify time'), default=now) # 最后修改时间
def save(self, *args, **kwargs):
"""
重写保存方法
处理文章浏览量更新和自动生成slug
"""
# 检查是否为文章视图更新操作(优化性能,避免完整保存)
is_update_views = isinstance(
self,
Article) and 'update_fields' in kwargs and kwargs['update_fields'] == ['views']
if is_update_views:
# 只更新浏览量字段,提高性能
Article.objects.filter(pk=self.pk).update(views=self.views)
else:
# 自动生成slugURL友好字符串
if 'slug' in self.__dict__:
slug = getattr(
self, 'title') if 'title' in self.__dict__ else getattr(
self, 'name')
setattr(self, 'slug', slugify(slug))
# 调用父类保存方法
super().save(*args, **kwargs)
def get_full_url(self):
"""获取完整的URL地址包含域名"""
>>>>>>> hyt_branch
site = get_current_site().domain
url = "https://{site}{path}".format(site=site,
path=self.get_absolute_url())
return url
class Meta:
<<<<<<< HEAD
ordering = ['-id']
verbose_name = _('user')
verbose_name_plural = verbose_name
get_latest_by = 'id'
=======
from django.conf import settings
from django.db import models
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from blog.models import Article
# 评论模型,存储用户对文章的评论及评论间的嵌套关系
class Comment(models.Model):
body = models.TextField('正文', max_length=300) # 评论内容限制最大长度300字符
creation_time = models.DateTimeField(_('creation time'), default=now) # 评论创建时间,默认当前时间
last_modify_time = models.DateTimeField(_('last modify time'), default=now) # 评论最后修改时间,默认当前时间
author = models.ForeignKey(
settings.AUTH_USER_MODEL, # 关联Django内置用户模型便于扩展用户系统
verbose_name=_('author'),
on_delete=models.CASCADE) # 级联删除:用户删除时,其评论也会被删除
article = models.ForeignKey(
Article,
verbose_name=_('article'),
on_delete=models.CASCADE) # 级联删除:文章删除时,其下所有评论也会被删除
parent_comment = models.ForeignKey(
'self', # 自关联,实现评论嵌套回复功能
verbose_name=_('parent comment'),
blank=True,
null=True, # 允许为空,表示该评论是顶级评论(不是回复)
on_delete=models.CASCADE) # 级联删除:父评论删除时,其所有子评论也会被删除
is_enable = models.BooleanField(_('enable'),
default=False, blank=False, null=False) # 评论是否启用(可用于审核功能)
class Meta:
ordering = ['-id'] # 默认按ID降序排列最新评论显示在前面
verbose_name = _('comment')
verbose_name_plural = verbose_name
get_latest_by = 'id' # 指定通过id字段获取最新记录
def __str__(self):
return self.body
>>>>>>> zh_branch
=======
abstract = True # 抽象基类,不会创建数据库表
@abstractmethod
def get_absolute_url(self):
"""抽象方法获取对象的绝对URL子类必须实现"""
pass
class Article(BaseModel):
"""
文章模型
博客系统的核心模型存储所有文章内容
"""
# 文章状态选择
STATUS_CHOICES = (
('d', _('Draft')), # 草稿
('p', _('Published')), # 已发布
)
# 评论状态选择
COMMENT_STATUS = (
('o', _('Open')), # 开启评论
('c', _('Close')), # 关闭评论
)
# 内容类型选择
TYPE = (
('a', _('Article')), # 普通文章
('p', _('Page')), # 独立页面
)
# 基础字段
title = models.CharField(_('title'), max_length=200, unique=True) # 文章标题,唯一
body = MDTextField(_('body')) # 文章内容使用Markdown编辑器
pub_time = models.DateTimeField(_('publish time'), blank=False, null=False, default=now) # 发布时间
# 状态字段
status = models.CharField(_('status'), max_length=1, choices=STATUS_CHOICES, default='p') # 发布状态
comment_status = models.CharField(_('comment status'), max_length=1, choices=COMMENT_STATUS, default='o') # 评论状态
type = models.CharField(_('type'), max_length=1, choices=TYPE, default='a') # 内容类型
# 统计字段
views = models.PositiveIntegerField(_('views'), default=0) # 浏览次数
# 关联字段
author = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name=_('author'),
blank=False, null=False, on_delete=models.CASCADE) # 作者
article_order = models.IntegerField(_('order'), blank=False, null=False, default=0) # 文章排序
# 功能字段
show_toc = models.BooleanField(_('show toc'), blank=False, null=False, default=False) # 是否显示目录
category = models.ForeignKey('Category', verbose_name=_('category'),
on_delete=models.CASCADE, blank=False, null=False) # 分类
tags = models.ManyToManyField('Tag', verbose_name=_('tag'), blank=True) # 标签,多对多关系
def body_to_string(self):
"""将文章内容转换为字符串"""
return self.body
def __str__(self):
"""对象的字符串表示"""
return self.title
class Meta:
ordering = ['-article_order', '-pub_time'] # 默认按排序和发布时间降序排列
verbose_name = _('article') # 单数显示名称
verbose_name_plural = verbose_name # 复数显示名称
get_latest_by = 'id' # 获取最新记录的依据字段
def get_absolute_url(self):
"""获取文章的绝对URL包含年月日信息用于SEO"""
return reverse('blog:detailbyid', kwargs={
'article_id': self.id,
'year': self.creation_time.year,
'month': self.creation_time.month,
'day': self.creation_time.day
})
@cache_decorator(60 * 60 * 10) # 缓存10小时
def get_category_tree(self):
"""获取文章所属分类的树形结构,用于面包屑导航"""
tree = self.category.get_category_tree()
names = list(map(lambda c: (c.name, c.get_absolute_url()), tree))
return names
def save(self, *args, **kwargs):
"""保存文章,调用父类保存逻辑"""
super().save(*args, **kwargs)
def viewed(self):
"""增加文章浏览量使用update_fields优化性能"""
self.views += 1
self.save(update_fields=['views'])
def comment_list(self):
"""获取文章评论列表(带缓存)"""
cache_key = 'article_comments_{id}'.format(id=self.id)
value = cache.get(cache_key)
if value:
logger.info('get article comments:{id}'.format(id=self.id))
return value
else:
# 获取已启用的评论并按ID降序排列
comments = self.comment_set.filter(is_enable=True).order_by('-id')
cache.set(cache_key, comments, 60 * 100) # 缓存100分钟
logger.info('set article comments:{id}'.format(id=self.id))
return comments
def get_admin_url(self):
"""获取文章在Admin后台的URL"""
info = (self._meta.app_label, self._meta.model_name)
return reverse('admin:%s_%s_change' % info, args=(self.pk,))
@cache_decorator(expiration=60 * 100) # 缓存100分钟
def next_article(self):
"""获取下一篇文章按ID顺序"""
return Article.objects.filter(id__gt=self.id, status='p').order_by('id').first()
@cache_decorator(expiration=60 * 100) # 缓存100分钟
def prev_article(self):
"""获取上一篇文章按ID顺序"""
return Article.objects.filter(id__lt=self.id, status='p').first()
def get_first_image_url(self):
"""
从文章内容中提取第一张图片的URL
用于文章列表的缩略图显示
"""
# 使用正则表达式匹配Markdown图片语法
match = re.search(r'!\[.*?\]\((.+?)\)', self.body)
if match:
return match.group(1)
return ""
class Category(BaseModel):
"""
文章分类模型
用于组织和管理博客文章的类别支持多级分类结构
"""
name = models.CharField(_('category name'), max_length=30, unique=True) # 分类名称,唯一
parent_category = models.ForeignKey('self', verbose_name=_('parent category'),
blank=True, null=True, on_delete=models.CASCADE) # 父级分类,支持层级结构
slug = models.SlugField(default='no-slug', max_length=60, blank=True) # URL友好名称
index = models.IntegerField(default=0, verbose_name=_('index')) # 分类排序索引
class Meta:
ordering = ['-index'] # 按索引降序排列
verbose_name = _('category') # 单数显示名称
verbose_name_plural = verbose_name # 复数显示名称
def get_absolute_url(self):
"""获取分类的绝对URL地址使用slug作为URL参数"""
return reverse('blog:category_detail', kwargs={'category_name': self.slug})
def __str__(self):
"""对象的字符串表示"""
return self.name
@cache_decorator(60 * 60 * 10) # 缓存10小时
def get_category_tree(self):
"""
递归获得分类目录的父级
返回从当前分类到根分类的路径用于面包屑导航
"""
categorys = []
def parse(category):
categorys.append(category)
if category.parent_category:
parse(category.parent_category)
parse(self)
return categorys
@cache_decorator(60 * 60 * 10) # 缓存10小时
def get_sub_categorys(self):
"""
获得当前分类目录所有子集
返回所有子分类的列表
"""
categorys = []
all_categorys = Category.objects.all()
def parse(category):
if category not in categorys:
categorys.append(category)
childs = all_categorys.filter(parent_category=category)
for child in childs:
if category not in categorys:
categorys.append(child)
parse(child)
parse(self)
return categorys
class Tag(BaseModel):
"""文章标签模型"""
name = models.CharField(_('tag name'), max_length=30, unique=True) # 标签名称,唯一
slug = models.SlugField(default='no-slug', max_length=60, blank=True) # URL友好名称
def __str__(self):
return self.name
def get_absolute_url(self):
"""获取标签的绝对URL使用slug作为URL参数"""
return reverse('blog:tag_detail', kwargs={'tag_name': self.slug})
@cache_decorator(60 * 60 * 10) # 缓存10小时
def get_article_count(self):
"""获取该标签下的文章数量使用distinct去重"""
return Article.objects.filter(tags__name=self.name).distinct().count()
class Meta:
ordering = ['name'] # 按名称升序排列
verbose_name = _('tag') # 单数显示名称
verbose_name_plural = verbose_name # 复数显示名称
class Links(models.Model):
"""友情链接模型"""
name = models.CharField(_('link name'), max_length=30, unique=True) # 链接名称,唯一
link = models.URLField(_('link')) # 链接地址
sequence = models.IntegerField(_('order'), unique=True) # 显示顺序,唯一
is_enable = models.BooleanField(_('is show'), default=True, blank=False, null=False) # 是否启用
show_type = models.CharField(_('show type'), max_length=1, choices=LinkShowType.choices,
default=LinkShowType.I) # 显示类型
creation_time = models.DateTimeField(_('creation time'), default=now) # 创建时间
last_mod_time = models.DateTimeField(_('modify time'), default=now) # 最后修改时间
class Meta:
ordering = ['sequence'] # 按顺序升序排列
verbose_name = _('link') # 单数显示名称
verbose_name_plural = verbose_name # 复数显示名称
def __str__(self):
return self.name
class SideBar(models.Model):
"""侧边栏模型可以展示一些html内容"""
name = models.CharField(_('title'), max_length=100) # 侧边栏标题
content = models.TextField(_('content')) # 侧边栏内容HTML
sequence = models.IntegerField(_('order'), unique=True) # 显示顺序,唯一
is_enable = models.BooleanField(_('is enable'), default=True) # 是否启用
creation_time = models.DateTimeField(_('creation time'), default=now) # 创建时间
last_mod_time = models.DateTimeField(_('modify time'), default=now) # 最后修改时间
class Meta:
ordering = ['sequence'] # 按顺序升序排列
verbose_name = _('sidebar') # 单数显示名称
verbose_name_plural = verbose_name # 复数显示名称
def __str__(self):
return self.name
class BlogSettings(models.Model):
"""博客全局配置模型,使用单例模式确保只有一份配置"""
# 网站基本信息
site_name = models.CharField(_('site name'), max_length=200, null=False, blank=False, default='') # 网站名称
site_description = models.TextField(_('site description'), max_length=1000, null=False, blank=False,
default='') # 网站描述
site_seo_description = models.TextField(_('site seo description'), max_length=1000, null=False, blank=False,
default='') # SEO描述
site_keywords = models.TextField(_('site keywords'), max_length=1000, null=False, blank=False, default='') # 网站关键词
# 内容显示设置
article_sub_length = models.IntegerField(_('article sub length'), default=300) # 文章摘要长度
sidebar_article_count = models.IntegerField(_('sidebar article count'), default=10) # 侧边栏文章数量
sidebar_comment_count = models.IntegerField(_('sidebar comment count'), default=5) # 侧边栏评论数量
article_comment_count = models.IntegerField(_('article comment count'), default=5) # 文章页评论数量
# 广告设置
show_google_adsense = models.BooleanField(_('show adsense'), default=False) # 是否显示Google广告
google_adsense_codes = models.TextField(_('adsense code'), max_length=2000, null=True, blank=True,
default='') # 广告代码
# 功能开关
open_site_comment = models.BooleanField(_('open site comment'), default=True) # 是否开启全站评论
comment_need_review = models.BooleanField('评论是否需要审核', default=False, null=False) # 评论是否需要审核
# 页面布局
global_header = models.TextField("公共头部", null=True, blank=True, default='') # 全局头部HTML
global_footer = models.TextField("公共尾部", null=True, blank=True, default='') # 全局尾部HTML
# 备案信息
beian_code = models.CharField('备案号', max_length=2000, null=True, blank=True, default='') # ICP备案号
show_gongan_code = models.BooleanField('是否显示公安备案号', default=False, null=False) # 是否显示公安备案
gongan_beiancode = models.TextField('公安备案号', max_length=2000, null=True, blank=True, default='') # 公安备案号
# 统计代码
analytics_code = models.TextField("网站统计代码", max_length=1000, null=False, blank=False, default='') # 网站统计代码
class Meta:
verbose_name = _('Website configuration') # 单数显示名称
verbose_name_plural = verbose_name # 复数显示名称
def __str__(self):
return self.site_name
def clean(self):
"""验证配置唯一性,确保只有一个配置实例(单例模式)"""
if BlogSettings.objects.exclude(id=self.id).count():
raise ValidationError(_('There can only be one configuration'))
def save(self, *args, **kwargs):
"""保存配置并清除缓存,确保配置变更立即生效"""
super().save(*args, **kwargs)
from djangoblog.utils import cache
cache.clear() # 清除所有缓存
>>>>>>> hyt_branch

@ -0,0 +1,40 @@
from haystack import indexes
from blog.models import Article
class ArticleIndex(indexes.SearchIndex, indexes.Indexable):
"""
文章搜索索引类
这个类用于定义Django Haystack搜索引擎中文章的索引结构
它继承自SearchIndex和Indexable提供了文章模型的全文搜索功能
"""
# 主搜索字段document=True表示这是主要的搜索内容字段
# use_template=True表示使用模板文件来定义索引内容
text = indexes.CharField(document=True, use_template=True)
def get_model(self):
"""
获取与此索引关联的Django模型
Returns:
Model: 返回Article模型类
"""
return Article
def index_queryset(self, using=None):
"""
定义要建立索引的查询集
这个方法返回需要被索引的文章集合这里只索引已发布(status='p')的文章
草稿文章不会被包含在搜索索引中
Args:
using: 可选参数指定使用的搜索引擎别名
Returns:
QuerySet: 包含所有已发布文章的查询集
"""
return self.get_model().objects.filter(status='p')

@ -0,0 +1,399 @@
"""
Django settings for djangoblog project.
项目配置文件包含项目核心设置数据库中间件静态资源等所有全局配置
Generated by 'django-admin startproject' using Django 1.10.2.
"""
import os
import sys
from pathlib import Path
# 导入Django国际化翻译工具
from django.utils.translation import gettext_lazy as _
def env_to_bool(env, default):
"""
环境变量转换工具函数将环境变量的字符串值转为布尔值
- 若环境变量未设置返回默认值
- 若环境变量存在仅当值为'True'时返回True其他情况返回False
"""
str_val = os.environ.get(env)
return default if str_val is None else str_val == 'True'
# -------------------------- 基础路径配置 --------------------------
# 项目根目录:当前配置文件所在目录的父级目录(即项目根目录)
BASE_DIR = Path(__file__).resolve().parent.parent
# -------------------------- 安全与调试配置 --------------------------
# 快速开发配置(生产环境需修改)
# SECURITY WARNING: 生产环境必须将SECRET_KEY通过环境变量配置禁止硬编码
SECRET_KEY = os.environ.get(
'DJANGO_SECRET_KEY') or 'n9ceqv38)#&mwuat@(mjb_p%em$e8$qyr#fw9ot!=ba6lijx-6'
# 调试模式开发环境开启True生产环境必须关闭False
# 通过环境变量控制,默认开启调试
DEBUG = env_to_bool('DJANGO_DEBUG', True)
# 测试模式标识当执行python manage.py test时TESTING为True
TESTING = len(sys.argv) > 1 and sys.argv[1] == 'test'
# 允许访问的主机列表:生产环境需指定具体域名,禁止使用'*'(存在安全风险)
ALLOWED_HOSTS = ['*', '127.0.0.1', 'example.com']
# Django 4.0+新增信任的CSRF源列表防止跨站请求伪造生产环境需配置真实域名
CSRF_TRUSTED_ORIGINS = ['http://example.com']
# -------------------------- 应用配置 --------------------------
INSTALLED_APPS = [
# Django内置应用精简版Admin仅包含核心功能
'django.contrib.admin.apps.SimpleAdminConfig',
'django.contrib.auth', # 用户认证系统
'django.contrib.contenttypes', # 内容类型框架(关联模型与数据表)
'django.contrib.sessions', # 会话管理
'django.contrib.messages', # 消息提示系统
'django.contrib.staticfiles', # 静态资源管理
'django.contrib.sites', # 多站点支持用于sitemap等功能
'django.contrib.sitemaps', # 站点地图生成
# 第三方应用
'mdeditor', # Markdown编辑器用于文章编写
'haystack', # 全文搜索框架
'compressor', # 静态资源压缩CSS/JS合并压缩
# 自定义应用
'blog', # 博客核心功能(文章、分类等)
'accounts', # 用户账户管理(自定义用户模型等)
'comments', # 评论功能
'oauth', # 第三方登录如GitHub、微信等
'servermanager',# 服务器管理(如系统监控等)
'owntracks', # 位置追踪(可选功能)
'djangoblog' # 项目主应用(全局配置、工具函数等)
]
# -------------------------- 中间件配置 --------------------------
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware', # 安全相关中间件防XSS、点击劫持等
'django.contrib.sessions.middleware.SessionMiddleware', # 会话管理
'django.middleware.locale.LocaleMiddleware', # 国际化中间件(多语言支持)
'django.middleware.gzip.GZipMiddleware', # GZip压缩减少响应体积
# 'django.middleware.cache.UpdateCacheMiddleware', # 缓存更新(注释:当前未启用)
'django.middleware.common.CommonMiddleware', # 通用中间件处理URL、反向解析等
# 'django.middleware.cache.FetchFromCacheMiddleware', # 缓存读取(注释:当前未启用)
'django.middleware.csrf.CsrfViewMiddleware', # CSRF保护中间件
'django.contrib.auth.middleware.AuthenticationMiddleware', # 用户认证中间件
'django.contrib.messages.middleware.MessageMiddleware', # 消息提示中间件
'django.middleware.clickjacking.XFrameOptionsMiddleware', # 防点击劫持
'django.middleware.http.ConditionalGetMiddleware', # 处理HTTP条件请求如304缓存
'blog.middleware.OnlineMiddleware' # 自定义中间件(跟踪用户在线状态)
]
# -------------------------- URL与模板配置 --------------------------
# 项目主URL配置文件路径
ROOT_URLCONF = 'djangoblog.urls'
# 模板配置
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates', # Django模板引擎
'DIRS': [os.path.join(BASE_DIR, 'templates')], # 全局模板目录项目根目录下的templates
'APP_DIRS': True, # 允许从各应用的templates目录加载模板
'OPTIONS': {
# 模板上下文处理器:向所有模板注入全局变量
'context_processors': [
'django.template.context_processors.debug', # 调试模式变量
'django.template.context_processors.request', # 请求对象request
'django.contrib.auth.context_processors.auth', # 用户认证变量user
'django.contrib.messages.context_processors.messages', # 消息提示变量
'blog.context_processors.seo_processor' # 自定义SEO处理器注入SEO相关变量
],
},
},
]
# WSGI应用入口用于部署如Gunicorn、uWSGI
WSGI_APPLICATION = 'djangoblog.wsgi.application'
# -------------------------- 数据库配置 --------------------------
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql', # 数据库引擎MySQL
'NAME': os.environ.get('DJANGO_MYSQL_DATABASE') or 'djangoblog', # 数据库名
'USER': os.environ.get('DJANGO_MYSQL_USER') or 'root', # 数据库用户名
'PASSWORD': os.environ.get('DJANGO_MYSQL_PASSWORD') or 'LY181828', # 数据库密码
'HOST': os.environ.get('DJANGO_MYSQL_HOST') or '127.0.0.1', # 数据库主机
'PORT': int(os.environ.get('DJANGO_MYSQL_PORT') or 3306), # 数据库端口
'OPTIONS': {'charset': 'utf8mb4'}, # 数据库字符集支持emoji表情
}}
# -------------------------- 密码验证配置 --------------------------
AUTH_PASSWORD_VALIDATORS = [
# 验证密码与用户名/邮箱是否相似
{'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator'},
# 验证密码最小长度默认8位
{'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator'},
# 验证密码是否为常见弱密码如123456
{'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator'},
# 验证密码是否全为数字
{'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator'},
]
# -------------------------- 国际化与时间配置 --------------------------
# 支持的语言列表(英文、简体中文、繁体中文)
LANGUAGES = (
('en', _('English')),
('zh-hans', _('Simplified Chinese')),
('zh-hant', _('Traditional Chinese')),
)
# 语言文件目录(存放翻译文件的路径)
LOCALE_PATHS = (os.path.join(BASE_DIR, 'locale'),)
# 默认语言(简体中文)
LANGUAGE_CODE = 'zh-hans'
# 时区上海时区与UTC时差+8
TIME_ZONE = 'Asia/Shanghai'
# 启用国际化
USE_I18N = True
# 启用本地化(日期、时间格式等)
USE_L10N = True
# 禁用UTC时间使用本地时间存储数据库时间
USE_TZ = False
# -------------------------- 全文搜索配置Haystack --------------------------
HAYSTACK_CONNECTIONS = {
'default': {
# 搜索引擎自定义中文Whoosh引擎支持中文分词
'ENGINE': 'djangoblog.whoosh_cn_backend.WhooshEngine',
# 搜索索引存储路径项目配置文件目录下的whoosh_index
'PATH': os.path.join(os.path.dirname(__file__), 'whoosh_index'),
},
}
# 实时更新搜索索引:当文章新增/修改/删除时,自动更新搜索索引
HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor'
# -------------------------- 用户认证配置 --------------------------
# 自定义认证后端:允许用户用用户名或邮箱登录
AUTHENTICATION_BACKENDS = ['accounts.user_login_backend.EmailOrUsernameModelBackend']
# 自定义用户模型替换Django内置的User模型关联accounts应用的BlogUser
AUTH_USER_MODEL = 'accounts.BlogUser'
# 登录页面URL未登录用户访问需认证页面时重定向到该URL
LOGIN_URL = '/login/'
# -------------------------- 静态资源与媒体文件配置 --------------------------
# 静态资源收集目录生产环境使用python manage.py collectstatic收集后的路径
STATIC_ROOT = os.path.join(BASE_DIR, 'collectedstatic')
# 静态资源URL前缀前端访问静态资源的路径如http://example.com/static/
STATIC_URL = '/static/'
# 全局静态资源目录项目根目录下的static文件夹
STATICFILES = os.path.join(BASE_DIR, 'static')
# 媒体文件(用户上传文件,如文章图片)存储目录
MEDIA_ROOT = os.path.join(BASE_DIR, 'uploads')
# 媒体文件URL前缀前端访问上传文件的路径如http://example.com/media/
MEDIA_URL = '/media/'
# 静态资源查找器指定Django如何查找静态文件
STATICFILES_FINDERS = (
'django.contrib.staticfiles.finders.FileSystemFinder', # 从全局STATICFILES目录查找
'django.contrib.staticfiles.finders.AppDirectoriesFinder', # 从各应用的static目录查找
'compressor.finders.CompressorFinder', # 从compressor压缩后的目录查找
)
# -------------------------- 静态资源压缩配置Compressor --------------------------
# 启用压缩(生产环境建议开启,开发环境可关闭)
COMPRESS_ENABLED = True
# COMPRESS_OFFLINE = True # 离线压缩(注释:当前未启用,适合生产环境)
# CSS压缩过滤器1. 转换相对URL为绝对URL2. 压缩CSS代码
COMPRESS_CSS_FILTERS = [
'compressor.filters.css_default.CssAbsoluteFilter',
'compressor.filters.cssmin.CSSMinFilter'
]
# JS压缩过滤器压缩JS代码
COMPRESS_JS_FILTERS = ['compressor.filters.jsmin.JSMinFilter']
# -------------------------- 缓存配置 --------------------------
# HTTP缓存超时时间单位2592000秒=30天
CACHE_CONTROL_MAX_AGE = 2592000
# 默认缓存本地内存缓存适合开发环境生产环境建议用Redis/Memcached
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
'TIMEOUT': 10800, # 缓存超时时间3小时
'LOCATION': 'unique-snowflake', # 缓存实例标识(唯一即可)
}
}
# 若环境变量配置了Redis地址则使用Redis作为缓存生产环境推荐
if os.environ.get("DJANGO_REDIS_URL"):
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.redis.RedisCache',
'LOCATION': f'redis://{os.environ.get("DJANGO_REDIS_URL")}',
}
}
# -------------------------- 其他业务配置 --------------------------
# 多站点支持的站点ID默认1与django.contrib.sites配合使用
SITE_ID = 1
# 百度链接提交URL用于向百度搜索引擎提交新链接SEO优化
BAIDU_NOTIFY_URL = os.environ.get('DJANGO_BAIDU_NOTIFY_URL') or \
'http://data.zz.baidu.com/urls?site=https://www.lylinux.net&token=1uAOGrMsUm5syDGn'
# 时间格式:全局日期时间显示格式
TIME_FORMAT = '%Y-%m-%d %H:%M:%S'
DATE_TIME_FORMAT = '%Y-%m-%d'
# Bootstrap颜色样式列表用于文章标签、按钮等UI组件
BOOTSTRAP_COLOR_TYPES = ['default', 'primary', 'success', 'info', 'warning', 'danger']
# 分页配置:每页显示的文章数量
PAGINATE_BY = 10
# X-Frame-Options配置仅允许同域嵌入iframe防点击劫持
X_FRAME_OPTIONS = 'SAMEORIGIN'
# 默认自增字段类型Django 3.2+新增,避免主键溢出)
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
# 微信管理员密码两次MD5加密用于微信后台管理验证
WXADMIN = os.environ.get('DJANGO_WXADMIN_PASSWORD') or '995F03AC401D6CABABAEF756FC4D43C7'
# -------------------------- Elasticsearch配置可选 --------------------------
# 若环境变量配置了Elasticsearch地址则使用Elasticsearch作为搜索引擎替代Whoosh
if os.environ.get('DJANGO_ELASTICSEARCH_HOST'):
ELASTICSEARCH_DSL = {
'default': {'hosts': os.environ.get('DJANGO_ELASTICSEARCH_HOST')},
}
HAYSTACK_CONNECTIONS = {
'default': {'ENGINE': 'djangoblog.elasticsearch_backend.ElasticSearchEngine'},
}
# -------------------------- 日志配置 --------------------------
# 日志存储目录项目根目录下的logs文件夹
LOG_PATH = os.path.join(BASE_DIR, 'logs')
# 若目录不存在则创建exist_ok=True避免重复创建报错
if not os.path.exists(LOG_PATH):
os.makedirs(LOG_PATH, exist_ok=True)
LOGGING = {
'version': 1, # 日志配置版本固定为1
'disable_existing_loggers': False, # 不禁用已存在的日志器
'root': { # 根日志器(所有未指定日志器的日志都会走这里)
'level': 'INFO', # 日志级别INFO及以上会被记录
'handlers': ['console', 'log_file'], # 日志处理器(控制台+文件)
},
'formatters': { # 日志格式
'verbose': { # 详细格式(包含时间、级别、模块、行号等)
'format': '[%(asctime)s] %(levelname)s [%(name)s.%(funcName)s:%(lineno)d %(module)s] %(message)s',
}
},
'filters': { # 日志过滤器
'require_debug_false': {'()': 'django.utils.log.RequireDebugFalse'}, # 仅DEBUG=False时生效
'require_debug_true': {'()': 'django.utils.log.RequireDebugTrue'}, # 仅DEBUG=True时生效
},
'handlers': { # 日志处理器(定义日志如何输出)
'log_file': { # 文件处理器(按天分割日志)
'level': 'INFO',
'class': 'logging.handlers.TimedRotatingFileHandler', # 按时间轮转的文件处理器
'filename': os.path.join(LOG_PATH, 'djangoblog.log'), # 日志文件路径
'when': 'D', # 轮转周期(每天一个文件)
'formatter': 'verbose', # 使用详细格式
'interval': 1, # 轮转间隔1天
'delay': True, # 延迟创建文件(直到有日志时才创建)
'backupCount': 5, # 保留5个备份日志文件
'encoding': 'utf-8' # 日志文件编码
},
'console': { # 控制台处理器(仅开发环境显示)
'level': 'DEBUG',
'filters': ['require_debug_true'], # 仅DEBUG=True时生效
'class': 'logging.StreamHandler', # 输出到控制台
'formatter': 'verbose'
},
'null': {'class': 'logging.NullHandler'}, # 空处理器(丢弃日志)
'mail_admins': { # 邮件处理器发生ERROR时通知管理员
'level': 'ERROR',
'filters': ['require_debug_false'], # 仅生产环境DEBUG=False生效
'class': 'django.utils.log.AdminEmailHandler' # 发送邮件给ADMINS列表
}
},
'loggers': { # 自定义日志器(针对特定模块)
'djangoblog': { # 项目主模块日志
'handlers': ['log_file', 'console'],
'level': 'INFO',
'propagate': True, # 是否向上传递日志到root日志器
},
'django.request': { # Django请求相关日志如404、500错误
'handlers': ['mail_admins'],
'level': 'ERROR',
'propagate': False, # 不向上传递(避免重复记录)
}
}
}
# -------------------------- 邮件配置 --------------------------
# 邮件后端SMTP服务
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
# 是否使用TLS加密与SSL二选一根据邮件服务商配置
EMAIL_USE_TLS = env_to_bool('DJANGO_EMAIL_TLS', False)
# 是否使用SSL加密阿里云邮箱等常用465端口+SSL
EMAIL_USE_SSL = env_to_bool('DJANGO_EMAIL_SSL', True)
# 邮件服务器地址如阿里云邮箱为smtp.mxhichina.com
EMAIL_HOST = os.environ.get('DJANGO_EMAIL_HOST') or 'smtp.mxhichina.com'
# 邮件服务器端口SSL通常为465TLS通常为587
EMAIL_PORT = int(os.environ.get('DJANGO_EMAIL_PORT') or 465)
# 发送邮件的用户名(邮箱地址)
EMAIL_HOST_USER = os.environ.get('DJANGO_EMAIL_USER')
# 发送邮件的密码(邮箱授权码,非登录密码)
EMAIL_HOST_PASSWORD = os.environ.get('DJANGO_EMAIL_PASSWORD')
# 默认发件人与EMAIL_HOST_USER一致
DEFAULT_FROM_EMAIL = EMAIL_HOST_USER
# 管理员邮件与ADMINS配合使用
SERVER_EMAIL = EMAIL_HOST_USER
# 管理员列表发生ERROR时会收到邮件通知
ADMINS = [('admin', os.environ.get('DJANGO_ADMIN_EMAIL') or 'admin@admin.com')]
# -------------------------- 插件系统配置 --------------------------
# 插件目录项目根目录下的plugins文件夹
PLUGINS_DIR = BASE_DIR / 'plugins'
# 激活的插件列表(按需启用,提供额外功能)
ACTIVE_PLUGINS = [
'article_copyright', # 文章版权声明
'reading_time', # 文章阅读时长估算
'external_links', # 外部链接处理如添加nofollow
'view_count', # 文章阅读量统计
'seo_optimize' # SEO优化如自动生成meta标签
]

@ -1,223 +1,184 @@
{% load blog_tags %} <!-- 加载博客项目自定义的模板标签如处理Markdown的custom_markdown -->
{% load i18n %} <!-- 加载Django多语言工具用于后续文本的多语言切换 -->
<!-- 侧边栏容器class和role用于样式控制与无障碍访问表明该区域是辅助内容 -->
{% load blog_tags %} <!-- 加载自定义博客相关模板标签 -->
{% load i18n %} <!-- 加载国际化标签,支持多语言翻译 -->
<!-- 网站侧边栏容器role="complementary"标识为辅助内容区域 -->
<div id="secondary" class="widget-area" role="complementary">
<!-- 搜索框组件id和class用于定位与样式表明这是搜索类小部件 -->
<!-- 搜索框组件 -->
<aside id="search-2" class="widget widget_search">
<!-- 搜索表单,提交方式为GET数据提交到/search路径role="search"增强无障碍 -->
<!-- 搜索表单,请求方式为GET提交到/search路径 -->
<form role="search" method="get" id="searchform" class="searchform" action="/search">
<div>
<!-- 搜索框标签screen-reader-text类用于仅让屏幕阅读器识别不显示在页面 -->
<!-- 供屏幕阅读器识别的搜索标签,视觉上隐藏 -->
<label class="screen-reader-text" for="s">{% trans 'search' %}</label>
<!-- 搜索关键词输入框name="q"供后端获取关键词,初始值为空 -->
<!-- 搜索输入框name为"q"用于后端接收搜索关键词 -->
<input type="text" value="" name="q" id="q"/>
<!-- 搜索提交按钮id用于样式或JS绑定点击事件 -->
<!-- 搜索提交按钮 -->
<input type="submit" id="searchsubmit" />
</div>
</form>
</aside>
<!-- 条件判断若存在自定义侧边栏数据extra_sidebars则渲染该组件 -->
<!-- 额外侧边栏内容(若存在) -->
{% if extra_sidebars %}
<!-- 循环遍历所有自定义侧边栏,逐个渲染 -->
{% for sidebar in extra_sidebars %}
<!-- 自定义侧边栏容器class表明这是文本/HTML类小部件 -->
{% for sidebar in extra_sidebars %} <!-- 循环遍历所有额外侧边栏 -->
<!-- 自定义HTML侧边栏组件 -->
<aside class="widget_text widget widget_custom_html">
<!-- 自定义侧边栏标题,显示用户设置的侧边栏名称 -->
<p class="widget-title">{{ sidebar.name }}</p>
<!-- 自定义侧边栏内容容器,用于包裹处理后的内容 -->
<p class="widget-title">{{ sidebar.name }}</p> <!-- 侧边栏标题 -->
<div class="textwidget custom-html-widget">
<!-- 将侧边栏内容用custom_markdown转为HTML|safe允许渲染HTML代码需确保内容安全 -->
<!-- 渲染侧边栏内容经过自定义markdown处理并允许安全HTML -->
{{ sidebar.content|custom_markdown|safe }}
</div>
</aside>
{% endfor %}
{% endif %}
<!-- 条件判断若存在热门文章数据most_read_articles则渲染该组件 -->
<!-- 热门阅读文章(若存在) -->
{% if most_read_articles %}
<!-- 热门文章组件容器id和class用于定位与样式 -->
<aside id="views-4" class="widget widget_views">
<!-- 热门文章标题固定显示“Views”可改为多语言标签 -->
<p class="widget-title">Views</p>
<!-- 热门文章列表容器 -->
<p class="widget-title">Views</p> <!-- 组件标题(阅读量) -->
<ul>
<!-- 循环遍历热门文章数据,逐个渲染文章项 -->
{% for a in most_read_articles %}
{% for a in most_read_articles %} <!-- 循环遍历热门文章 -->
<li>
<!-- 文章标题链接调用文章模型的get_absolute_url生成详情页地址title属性为文章标题鼠标悬浮显示 -->
<!-- 文章链接,标题为文章标题 -->
<a href="{{ a.get_absolute_url }}" title="{{ a.title }}">
{{ a.title }} <!-- 显示文章标题 -->
</a> - {{ a.views }} views <!-- 显示文章阅读量后缀“views”为固定文本 -->
{{ a.title }}
</a> - {{ a.views }} views <!-- 显示文章阅读量 -->
</li>
{% endfor %}
</ul>
</aside>
{% endif %}
<!-- 条件判断若存在分类数据sidebar_categorys则渲染该组件 -->
<!-- 文章分类侧边栏(若存在) -->
{% if sidebar_categorys %}
<!-- 分类组件容器id和class用于定位与样式 -->
<aside id="su_siloed_terms-2" class="widget widget_su_siloed_terms">
<!-- 分类标题,用{% trans 'category' %}实现多语言中文显示“分类”英文显示“Category” -->
<p class="widget-title">{% trans 'category' %}</p>
<!-- 分类列表容器 -->
<p class="widget-title">{% trans 'category' %}</p> <!-- 分类标题(多语言) -->
<ul>
<!-- 循环遍历分类数据,逐个渲染分类项 -->
{% for c in sidebar_categorys %}
<!-- 分类项cat-item类用于样式每个分类项是一个链接 -->
{% for c in sidebar_categorys %} <!-- 循环遍历分类 -->
<li class="cat-item cat-item-184">
<!-- 分类名称链接调用分类模型的get_absolute_url生成分类页地址 -->
<!-- 分类链接,指向分类详情页 -->
<a href={{ c.get_absolute_url }}>{{ c.name }}</a>
</li>
{% endfor %}
</ul>
</aside>
{% endif %}
<!-- 条件判断若存在评论数据sidebar_comments且开启网站评论open_site_comment则渲染该组件 -->
<!-- 最新评论(若存在且网站允许评论) -->
{% if sidebar_comments and open_site_comment %}
<!-- 最新评论组件容器id和class用于定位与样式 -->
<aside id="ds-recent-comments-4" class="widget ds-widget-recent-comments">
<!-- 最新评论标题,用{% trans 'recent comments' %}实现多语言中文“最新评论”英文“Recent Comments” -->
<p class="widget-title">{% trans 'recent comments' %}</p>
<!-- 最新评论列表容器id用于定位 -->
<p class="widget-title">{% trans 'recent comments' %}</p> <!-- 最新评论标题 -->
<ul id="recentcomments">
<!-- 循环遍历评论数据,逐个渲染评论项 -->
{% for c in sidebar_comments %}
{% for c in sidebar_comments %} <!-- 循环遍历最新评论 -->
<li class="recentcomments">
<!-- 评论作者名称,显示评论用户的用户名 -->
<span class="comment-author-link">{{ c.author.username }}</span>
<!-- 评论归属文本,用{% trans 'published on' %}实现多语言中文“发表于”英文“published on” -->
{% trans 'published on' %}《
<!-- 评论所属文章链接,生成文章详情页地址并通过#comment-{{ c.pk }}锚点定位到该评论 -->
<span class="comment-author-link">{{ c.author.username }}</span> <!-- 评论作者 -->
{% trans 'published on' %}《 <!-- 翻译为“发表于” -->
<!-- 链接到评论所在的文章,锚点定位到具体评论 -->
<a href="{{ c.article.get_absolute_url }}#comment-{{ c.pk }}">{{ c.article.title }}</a>
</li>
{% endfor %}
</ul>
</aside>
{% endif %}
<!-- 条件判断若存在最新文章数据recent_articles则渲染该组件 -->
<!-- 最新文章(若存在) -->
{% if recent_articles %}
<!-- 最新文章组件容器id和class用于定位与样式 -->
<aside id="recent-posts-2" class="widget widget_recent_entries">
<!-- 最新文章标题,用{% trans 'recent articles' %}实现多语言中文“最新文章”英文“Recent Articles” -->
<p class="widget-title">{% trans 'recent articles' %}</p>
<!-- 最新文章列表容器 -->
<p class="widget-title">{% trans 'recent articles' %}</p> <!-- 最新文章标题 -->
<ul>
<!-- 循环遍历最新文章数据,逐个渲染文章项 -->
{% for a in recent_articles %}
{% for a in recent_articles %} <!-- 循环遍历最新文章 -->
<li>
<!-- 文章标题链接生成详情页地址title属性为文章标题鼠标悬浮显示 -->
<!-- 文章链接,标题为文章标题 -->
<a href="{{ a.get_absolute_url }}" title="{{ a.title }}">
{{ a.title }} <!-- 显示文章标题 -->
{{ a.title }}
</a>
</li>
{% endfor %}
</ul>
</aside>
{% endif %}
<!-- 条件判断若存在书签链接数据sidabar_links注意此处变量名可能为笔误应为sidebar_links则渲染该组件 -->
<!-- 收藏链接(若存在) -->
{% if sidabar_links %}
<!-- 书签链接组件容器id和class用于定位与样式 -->
<aside id="linkcat-0" class="widget widget_links">
<!-- 书签链接标题,用{% trans 'bookmark' %}实现多语言中文“书签”英文“Bookmark” -->
<p class="widget-title">{% trans 'bookmark' %}</p>
<!-- 书签链接列表容器class用于样式 -->
<p class="widget-title">{% trans 'bookmark' %}</p> <!-- 收藏标题 -->
<ul class='xoxo blogroll'>
<!-- 循环遍历书签链接数据,逐个渲染链接项 -->
{% for l in sidabar_links %}
{% for l in sidabar_links %} <!-- 循环遍历收藏链接 -->
<li>
<!-- 书签链接target="_blank"表示在新窗口打开title属性为书签名称鼠标悬浮显示 -->
<!-- 收藏链接,新窗口打开,标题为链接名称 -->
<a href="{{ l.link }}" target="_blank" title="{{ l.name }}">{{ l.name }}</a>
</li>
{% endfor %}
</ul>
</aside>
{% endif %}
<!-- 条件判断若开启Google广告show_google_adsense则渲染该组件 -->
<!-- Google广告若开启 -->
{% if show_google_adsense %}
<!-- Google广告组件容器id和class用于定位与样式 -->
<aside id="text-2" class="widget widget_text">
<!-- 广告标题固定显示“Google AdSense” -->
<p class="widget-title">Google AdSense</p>
<!-- 广告内容容器 -->
<p class="widget-title">Google AdSense</p> <!-- 广告标题 -->
<div class="textwidget">
<!-- 渲染Google广告代码|safe允许执行广告的HTML/JS代码需确保代码来源安全 -->
<!-- 渲染Google广告代码允许安全HTML -->
{{ google_adsense_codes|safe }}
</div>
</aside>
{% endif %}
<!-- 条件判断若存在标签数据sidebar_tags则渲染该组件 -->
<!-- 标签云(若存在) -->
{% if sidebar_tags %}
<!-- 标签云组件容器id和class用于定位与样式 -->
<aside id="tag_cloud-2" class="widget widget_tag_cloud">
<!-- 标签云标题,用{% trans 'Tag Cloud' %}实现多语言中文“标签云”英文“Tag Cloud” -->
<p class="widget-title">{% trans 'Tag Cloud' %}</p>
<!-- 标签云容器,用于包裹所有标签 -->
<p class="widget-title">{% trans 'Tag Cloud' %}</p> <!-- 标签云标题 -->
<div class="tagcloud">
<!-- 循环遍历标签数据tag=标签对象count=标签关联文章数size=标签字体大小) -->
{% for tag,count,size in sidebar_tags %}
<!-- 标签链接生成标签页地址class用于样式title属性显示标签关联文章数 -->
{% for tag,count,size in sidebar_tags %} <!-- 循环遍历标签,获取标签、数量、字体大小 -->
<!-- 标签链接字体大小由size变量控制标题显示标签下文章数量 -->
<a href="{{ tag.get_absolute_url }}"
class="tag-link-{{ tag.id }} tag-link-position-{{ tag.id }}"
style="font-size: {{ size }}pt;" <!-- 动态设置标签字体大小,关联文章越多字体越大 -->
title="{{ count }}个话题"> {{ tag.name }} <!-- 显示标签名称title属性文本可改为多语言 -->
style="font-size: {{ size }}pt;" title="{{ count }}个话题"> {{ tag.name }}
</a>
{% endfor %}
</div>
</aside>
{% endif %}
<!-- GitHub仓库推广组件容器id和class用于定位与样式id="text-2"与广告组件重复建议修改为唯一id -->
<!-- 网站源码Star/Fork提示 -->
<aside id="text-2" class="widget widget_text">
<!-- 推广标题,用{% trans %}实现多语言中文“欢迎Star或Fork本网站源码”英文对应翻译 -->
<p class="widget-title">{% trans 'Welcome to star or fork the source code of this site' %}</p>
<!-- 推广内容容器 -->
<p class="widget-title">{% trans 'Welcome to star or fork the source code of this site' %}</p> <!-- 多语言提示文本 -->
<div class="textwidget">
<p>
<!-- GitHub Star按钮链接指向项目仓库rel="nofollow"避免搜索引擎追踪 -->
<a href="https://github.com/liangliangyy/DjangoBlog" rel="nofollow">
<!-- 加载GitHub Star数量图标实时显示项目Star数 -->
<img src="https://resource.lylinux.net/img.shields.io/github/stars/liangliangyy/djangoblog.svg?style=social&amp;label=Star" alt="GitHub stars">
</a>
<!-- GitHub Fork按钮链接指向项目仓库 -->
<a href="https://github.com/liangliangyy/DjangoBlog" rel="nofollow">
<!-- 加载GitHub Fork数量图标实时显示项目Fork数 -->
<img src="https://resource.lylinux.net/img.shields.io/github/forks/liangliangyy/djangoblog.svg?style=social&amp;label=Fork" alt="GitHub forks">
</a>
<!-- GitHub Star按钮图片链接 -->
<a href="https://github.com/liangliangyy/DjangoBlog" rel="nofollow"><img
src="https://resource.lylinux.net/img.shields.io/github/stars/liangliangyy/djangoblog.svg?style=social&amp;label=Star"
alt="GitHub stars"></a>
<!-- GitHub Fork按钮图片链接 -->
<a href="https://github.com/liangliangyy/DjangoBlog" rel="nofollow"><img
src="https://resource.lylinux.net/img.shields.io/github/forks/liangliangyy/djangoblog.svg?style=social&amp;label=Fork"
alt="GitHub forks"></a>
</p>
</div>
</aside>
<!-- 功能链接组件容器id和class用于定位与样式 -->
<!-- 功能链接侧边栏 -->
<aside id="meta-3" class="widget widget_meta">
<!-- 功能链接标题,用{% trans 'Function' %}实现多语言中文“功能”英文“Function” -->
<p class="widget-title">{% trans 'Function' %}</p>
<!-- 功能链接列表容器 -->
<p class="widget-title">{% trans 'Function' %}</p> <!-- 功能标题 -->
<ul>
<!-- 管理后台链接,固定指向/admin/路径rel="nofollow"避免搜索引擎追踪 -->
<!-- 后台管理系统链接 -->
<li><a href="/admin/" rel="nofollow">{% trans 'management site' %}</a></li>
<!-- 条件判断若用户已登录user.is_authenticated显示“退出”链接 -->
{% if user.is_authenticated %}
<li>
<!-- 退出链接,通过{% url "account:logout" %}生成退出接口地址 -->
<a href="{% url "account:logout" %}" rel="nofollow">{% trans 'logout' %}</a>
</li>
<!-- 若用户未登录,显示“登录”链接 -->
{% if user.is_authenticated %} <!-- 若用户已登录 -->
<!-- 登出链接 -->
<li><a href="{% url "account:logout" %}" rel="nofollow">{% trans 'logout' %}</a></li>
{% else %}
<li>
<!-- 登录链接,通过{% url "account:login" %}生成登录页面地址 -->
<a href="{% url "account:login" %}" rel="nofollow">{% trans 'login' %}</a>
</li>
<!-- 登录链接 -->
<li><a href="{% url "account:login" %}" rel="nofollow">{% trans 'login' %}</a></li>
{% endif %}
<!-- 条件判断若用户是超级管理员user.is_superuser显示“轨迹记录”链接 -->
{% if user.is_superuser %}
<li>
<!-- 轨迹记录链接,通过{% url 'owntracks:show_dates' %}生成对应页面地址target="_blank"新窗口打开 -->
<a href="{% url 'owntracks:show_dates' %}" target="_blank">{% trans 'Track record' %}</a>
</li>
{% if user.is_superuser %} <!-- 若用户是超级管理员 -->
<!-- 轨迹记录链接(新窗口打开) -->
<li><a href="{% url 'owntracks:show_dates' %}" target="_blank">{% trans 'Track record' %}</a></li>
{% endif %}
<!-- GitBook链接固定指向http://gitbook.lylinux.net新窗口打开 -->
<!-- GitBook链接新窗口打开 -->
<li><a href="http://gitbook.lylinux.net" target="_blank" rel="nofollow">GitBook</a></li>
</ul>
</aside>
<!-- 回到顶部按钮,id="rocket"用于JS绑定点击事件class="show"表示初始显示状态 -->
<!-- 回到顶部按钮,初始显示,鼠标悬停提示文本 -->
<div id="rocket" class="show" title="{% trans 'Click me to return to the top' %}"></div>
</div><!-- #secondary --> <!-- 侧边栏容器结束注释 -->
</div><!-- #secondary --> <!-- 侧边栏容器结束 -->

@ -0,0 +1,124 @@
# 导入Django站点地图核心类和URL反向解析工具
from django.contrib.sitemaps import Sitemap
from django.urls import reverse
# 导入博客相关模型(文章、分类、标签),用于生成动态页面的站点地图
from blog.models import Article, Category, Tag
class StaticViewSitemap(Sitemap):
"""
静态页面站点地图类用于生成网站静态页面如首页的Sitemap条目
Sitemap用于告知搜索引擎网站的页面结构帮助其抓取和索引
"""
# 页面优先级0.0-1.0):值越高,搜索引擎认为该页面越重要
priority = 0.5
# 页面内容更新频率可选值包括always、hourly、daily、weekly、monthly、yearly、never
changefreq = 'daily'
def items(self):
"""
定义需要包含在站点地图中的静态页面URL名称列表
返回的是Django URL配置中定义的'name'属性'blog:index'对应首页URL
"""
return ['blog:index', ]
def location(self, item):
"""
为items()返回的每个URL名称生成对应的绝对URL
通过reverse()方法解析URL名称获取实际访问路径'blog:index'解析为'/'
"""
return reverse(item)
class ArticleSiteMap(Sitemap):
"""
文章页面站点地图类用于生成所有已发布文章的Sitemap条目
"""
# 文章页面更新频率:每月(因单篇文章发布后修改频率较低)
changefreq = "monthly"
# 文章页面优先级0.6(高于分类/标签,低于首页)
priority = "0.6"
def items(self):
"""
返回所有状态为"已发布"status='p'的文章对象
仅包含已发布文章避免搜索引擎抓取草稿或未公开内容
"""
return Article.objects.filter(status='p')
def lastmod(self, obj):
"""
定义每个文章页面的最后修改时间
取值为文章的last_modify_time字段文章最后更新的时间
帮助搜索引擎识别页面是否有更新决定是否重新抓取
"""
return obj.last_modify_time
class CategorySiteMap(Sitemap):
"""
分类页面站点地图类用于生成所有文章分类页面的Sitemap条目
"""
# 分类页面更新频率:每周(分类下文章新增/删除会影响分类页,频率低于文章)
changefreq = "Weekly"
# 分类页面优先级0.6(与文章同级,高于标签/用户)
priority = "0.6"
def items(self):
"""返回所有分类对象生成每个分类页面的Sitemap条目"""
return Category.objects.all()
def lastmod(self, obj):
"""
分类页面的最后修改时间取值为分类的last_modify_time字段
分类信息如分类名称修改时会更新该时间提示搜索引擎重新抓取
"""
return obj.last_modify_time
class TagSiteMap(Sitemap):
"""
标签页面站点地图类用于生成所有文章标签页面的Sitemap条目
"""
# 标签页面更新频率:每周(标签下文章增减频率较低)
changefreq = "Weekly"
# 标签页面优先级0.3(低于首页、文章、分类,属于次要导航页面)
priority = "0.3"
def items(self):
"""返回所有标签对象生成每个标签页面的Sitemap条目"""
return Tag.objects.all()
def lastmod(self, obj):
"""
标签页面的最后修改时间取值为标签的last_modify_time字段
标签信息如标签名称修改时更新用于搜索引擎判断页面新鲜度
"""
return obj.last_modify_time
class UserSiteMap(Sitemap):
"""
用户页面站点地图类用于生成所有发布过文章的用户主页的Sitemap条目
"""
# 用户页面更新频率:每周(用户发布新文章或修改资料时才更新)
changefreq = "Weekly"
# 用户页面优先级0.3(与标签同级,属于次要页面)
priority = "0.3"
def items(self):
"""
返回所有发布过文章的独特用户对象
1. 通过Article.objects.all()获取所有文章提取每篇文章的author作者
2. 使用map()遍历文章列表获取作者集合用set()去重避免重复用户
3. 转换为list()符合items()返回可迭代对象的要求
"""
return list(set(map(lambda x: x.author, Article.objects.all())))
def lastmod(self, obj):
"""
用户页面的最后修改时间取值为用户的date_joined字段用户注册时间
此处可根据需求调整如改为用户最后发布文章时间或最后修改资料时间
"""
return obj.date_joined

@ -0,0 +1,49 @@
# 导入日志模块,用于记录通知过程中的信息和错误
import logging
# 导入requests库用于发送HTTP请求向搜索引擎提交链接
import requests
# 导入Django项目配置用于获取百度链接提交的URL在settings.py中配置
from django.conf import settings
# 创建当前模块的日志记录器,用于输出通知相关的日志
logger = logging.getLogger(__name__)
class SpiderNotify():
"""
搜索引擎爬虫通知类用于向搜索引擎当前仅百度提交网站新链接
帮助搜索引擎快速发现并收录网站新增或更新的页面提升SEO效果
"""
@staticmethod
def baidu_notify(urls):
"""
向百度搜索引擎提交链接的静态方法
通过百度链接提交APIBAIDU_NOTIFY_URL将新页面URL推送给百度爬虫
Args:
urls (list): 需要提交的URL列表每个元素为一个页面的完整URL或相对URL
"""
try:
# 将URL列表转换为以换行符分隔的字符串符合百度API的提交格式要求
data = '\n'.join(urls)
# 发送POST请求到百度链接提交URL提交URL数据
result = requests.post(settings.BAIDU_NOTIFY_URL, data=data)
# 记录百度返回的响应结果(成功时包含提交状态,用于调试和审计)
logger.info(result.text)
except Exception as e:
# 捕获请求过程中的所有异常如网络错误、API地址错误等记录错误日志
logger.error(e)
@staticmethod
def notify(url):
"""
通用通知入口静态方法统一调用百度链接提交方法
此处为简化设计后续可扩展支持其他搜索引擎如谷歌必应
Args:
url (str/list): 单个URL字符串或URL列表最终会转为列表提交给百度
"""
# 调用百度链接提交方法实现URL推送
SpiderNotify.baidu_notify(url)

@ -0,0 +1,859 @@
<<<<<<< HEAD
<<<<<<< HEAD
<<<<<<< HEAD
from django.test import TestCase
from djangoblog.utils import *
class DjangoBlogTest(TestCase):
def setUp(self):
pass
def test_utils(self):
md5 = get_sha256('test')
self.assertIsNotNone(md5)
c = CommonMarkdown.get_markdown('''
# Title1
```python
import os
```
[url](https://www.lylinux.net/)
[ddd](http://www.baidu.com)
''')
self.assertIsNotNone(c)
d = {
'd': 'key1',
'd2': 'key2'
}
data = parse_dict_to_url(d)
self.assertIsNotNone(data)
from django.test import Client, RequestFactory, TestCase
from django.urls import reverse
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from accounts.models import BlogUser
from blog.models import Article, Category
from djangoblog.utils import *
from . import utils
=======
import os
from django.conf import settings
from django.core.files.uploadedfile import SimpleUploadedFile
from django.core.management import call_command
from django.core.paginator import Paginator
from django.templatetags.static import static
from django.test import Client, RequestFactory, TestCase
from django.urls import reverse
from django.utils import timezone
from accounts.models import BlogUser
from blog.forms import BlogSearchForm
from blog.models import Article, Category, Tag, SideBar, Links
from blog.templatetags.blog_tags import load_pagination_info, load_articletags
from djangoblog.utils import get_current_site, get_sha256
from oauth.models import OAuthUser, OAuthConfig
>>>>>>> hyt_branch
# Create your tests here.
<<<<<<< HEAD
class AccountTest(TestCase):
def setUp(self):
self.client = Client()
self.factory = RequestFactory()
self.blog_user = BlogUser.objects.create_user(
username="test",
email="admin@admin.com",
password="12345678"
)
self.new_test = "xxx123--="
def test_validate_account(self):
site = get_current_site().domain
user = BlogUser.objects.create_superuser(
email="liangliangyy1@gmail.com",
username="liangliangyy1",
password="qwer!@#$ggg")
testuser = BlogUser.objects.get(username='liangliangyy1')
loginresult = self.client.login(
username='liangliangyy1',
password='qwer!@#$ggg')
self.assertEqual(loginresult, True)
response = self.client.get('/admin/')
self.assertEqual(response.status_code, 200)
category = Category()
category.name = "categoryaaa"
category.creation_time = timezone.now()
category.last_modify_time = timezone.now()
category.save()
article = Article()
article.title = "nicetitleaaa"
article.body = "nicecontentaaa"
article.author = user
article.category = category
article.type = 'a'
article.status = 'p'
article.save()
response = self.client.get(article.get_admin_url())
self.assertEqual(response.status_code, 200)
def test_validate_register(self):
=======
from django.test import Client, RequestFactory, TestCase
from django.urls import reverse
# 导入时区处理模块,用于处理时间相关数据
from django.utils import timezone
# 导入国际化翻译函数,用于多语言文本
from django.utils.translation import gettext_lazy as _
# 导入用户模型,用于创建测试用户数据
from accounts.models import BlogUser
# 导入文章、分类模型,用于创建测试内容数据
from blog.models import Article, Category
# 导入项目工具函数,用于测试通用功能
from djangoblog.utils import *
# 导入当前应用accounts的工具函数用于测试账号相关工具功能
from . import utils
# 定义账号功能测试类继承TestCase基础测试用例类
class AccountTest(TestCase):
# 测试前初始化方法,每个测试方法执行前自动运行
def setUp(self):
# 初始化测试客户端用于模拟用户发起HTTP请求
self.client = Client()
# 初始化请求工厂,用于构造自定义请求对象
self.factory = RequestFactory()
# 创建普通测试用户,存入测试数据库
self.blog_user = BlogUser.objects.create_user(
username="test", # 用户名
email="admin@admin.com", # 邮箱
password="12345678" # 密码
)
# 定义测试用的新密码字符串,用于后续密码修改测试
self.new_test = "xxx123--="
# 测试账号验证功能(登录、管理员权限、文章管理)
def test_validate_account(self):
# 获取当前站点域名(用于测试环境下的域名相关逻辑)
site = get_current_site().domain
# 创建超级用户,用于测试管理员权限
user = BlogUser.objects.create_superuser(
email="liangliangyy1@gmail.com", # 超级用户邮箱
username="liangliangyy1", # 超级用户名
password="qwer!@#$ggg") # 超级用户密码
# 从数据库中查询刚创建的超级用户,用于后续验证
testuser = BlogUser.objects.get(username='liangliangyy1')
# 模拟超级用户登录,返回登录结果(布尔值)
loginresult = self.client.login(
username='liangliangyy1', # 登录用户名
password='qwer!@#$ggg') # 登录密码
# 断言登录结果应为True登录成功
self.assertEqual(loginresult, True)
# 模拟超级用户访问管理员后台首页
response = self.client.get('/admin/')
# 断言响应状态码应为200访问成功
self.assertEqual(response.status_code, 200)
# 创建测试分类,用于后续文章关联
category = Category()
category.name = "categoryaaa" # 分类名称
category.creation_time = timezone.now() # 分类创建时间(当前时间)
category.last_modify_time = timezone.now() # 分类最后修改时间(当前时间)
category.save() # 保存分类到测试数据库
# 创建测试文章,关联上述分类和超级用户
article = Article()
article.title = "nicetitleaaa" # 文章标题
article.body = "nicecontentaaa" # 文章内容
article.author = user # 文章作者(超级用户)
article.category = category # 文章所属分类
article.type = 'a' # 文章类型(假设'a'代表普通文章)
article.status = 'p' # 文章状态(假设'p'代表已发布)
article.save() # 保存文章到测试数据库
# 模拟访问该文章的管理员编辑页通过文章模型的自定义方法获取URL
response = self.client.get(article.get_admin_url())
# 断言响应状态码应为200管理员有权限访问访问成功
self.assertEqual(response.status_code, 200)
# 测试账号注册功能(注册、邮箱验证、登录、权限提升、文章管理、登出)
def test_validate_register(self):
# 断言:数据库中初始不存在邮箱为'user123@user.com'的用户计数为0
>>>>>>> zh_branch
self.assertEquals(
0, len(
BlogUser.objects.filter(
email='user123@user.com')))
<<<<<<< HEAD
response = self.client.post(reverse('account:register'), {
'username': 'user1233',
'email': 'user123@user.com',
'password1': 'password123!q@wE#R$T',
'password2': 'password123!q@wE#R$T',
})
=======
# 模拟POST请求提交注册表单访问注册接口
response = self.client.post(reverse('account:register'), {
'username': 'user1233', # 注册用户名
'email': 'user123@user.com', # 注册邮箱
'password1': 'password123!q@wE#R$T', # 注册密码
'password2': 'password123!q@wE#R$T', # 密码确认(与密码一致)
})
# 断言注册后数据库中应存在该邮箱用户计数为1
>>>>>>> zh_branch
self.assertEquals(
1, len(
BlogUser.objects.filter(
email='user123@user.com')))
<<<<<<< HEAD
user = BlogUser.objects.filter(email='user123@user.com')[0]
sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
path = reverse('accounts:result')
url = '{path}?type=validation&id={id}&sign={sign}'.format(
path=path, id=user.id, sign=sign)
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.client.login(username='user1233', password='password123!q@wE#R$T')
user = BlogUser.objects.filter(email='user123@user.com')[0]
user.is_superuser = True
user.is_staff = True
user.save()
delete_sidebar_cache()
category = Category()
category.name = "categoryaaa"
category.creation_time = timezone.now()
category.last_modify_time = timezone.now()
category.save()
article = Article()
article.category = category
article.title = "nicetitle333"
article.body = "nicecontentttt"
article.author = user
article.type = 'a'
article.status = 'p'
article.save()
response = self.client.get(article.get_admin_url())
self.assertEqual(response.status_code, 200)
response = self.client.get(reverse('account:logout'))
self.assertIn(response.status_code, [301, 302, 200])
response = self.client.get(article.get_admin_url())
self.assertIn(response.status_code, [301, 302, 200])
response = self.client.post(reverse('account:login'), {
'username': 'user1233',
'password': 'password123'
})
self.assertIn(response.status_code, [301, 302, 200])
response = self.client.get(article.get_admin_url())
self.assertIn(response.status_code, [301, 302, 200])
def test_verify_email_code(self):
to_email = "admin@admin.com"
code = generate_code()
utils.set_code(to_email, code)
utils.send_verify_email(to_email, code)
err = utils.verify("admin@admin.com", code)
self.assertEqual(err, None)
err = utils.verify("admin@123.com", code)
self.assertEqual(type(err), str)
def test_forget_password_email_code_success(self):
resp = self.client.post(
path=reverse("account:forget_password_code"),
data=dict(email="admin@admin.com")
)
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.content.decode("utf-8"), "ok")
def test_forget_password_email_code_fail(self):
resp = self.client.post(
path=reverse("account:forget_password_code"),
data=dict()
)
self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱")
resp = self.client.post(
path=reverse("account:forget_password_code"),
data=dict(email="admin@com")
)
self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱")
def test_forget_password_email_success(self):
code = generate_code()
utils.set_code(self.blog_user.email, code)
data = dict(
new_password1=self.new_test,
new_password2=self.new_test,
email=self.blog_user.email,
code=code,
)
resp = self.client.post(
path=reverse("account:forget_password"),
data=data
)
self.assertEqual(resp.status_code, 302)
# 验证用户密码是否修改成功
blog_user = BlogUser.objects.filter(
email=self.blog_user.email,
).first() # type: BlogUser
self.assertNotEqual(blog_user, None)
self.assertEqual(blog_user.check_password(data["new_password1"]), True)
def test_forget_password_email_not_user(self):
data = dict(
new_password1=self.new_test,
new_password2=self.new_test,
email="123@123.com",
code="123456",
)
=======
# 从数据库中查询刚注册的用户
user = BlogUser.objects.filter(email='user123@user.com')[0]
# 生成用户邮箱验证的签名双重SHA256加密结合密钥和用户ID
sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
# 反向解析验证结果页的URL
path = reverse('accounts:result')
# 拼接完整的邮箱验证URL包含用户ID和签名
url = '{path}?type=validation&id={id}&sign={sign}'.format(
path=path, id=user.id, sign=sign)
# 模拟访问邮箱验证URL完成验证
response = self.client.get(url)
# 断言验证页面访问成功状态码200
self.assertEqual(response.status_code, 200)
# 模拟刚注册的用户登录
self.client.login(username='user1233', password='password123!q@wE#R$T')
# 重新查询该用户,准备提升权限
user = BlogUser.objects.filter(email='user123@user.com')[0]
user.is_superuser = True # 设置为超级用户
user.is_staff = True # 设置为管理员有权访问admin后台
user.save() # 保存权限修改
# 调用工具函数删除侧边栏缓存(避免缓存影响测试结果)
delete_sidebar_cache()
# 创建测试分类(用于后续文章关联)
category = Category()
category.name = "categoryaaa" # 分类名称
category.creation_time = timezone.now() # 创建时间
category.last_modify_time = timezone.now() # 最后修改时间
category.save() # 保存分类
# 创建测试文章(关联上述分类和提升权限后的用户)
article = Article()
article.category = category # 所属分类
article.title = "nicetitle333" # 文章标题
article.body = "nicecontentttt" # 文章内容
article.author = user # 文章作者(提升权限后的用户)
article.type = 'a' # 文章类型
article.status = 'p' # 文章状态(已发布)
article.save() # 保存文章
# 模拟访问该文章的管理员编辑页
response = self.client.get(article.get_admin_url())
# 断言访问成功状态码200因用户已提升为管理员
self.assertEqual(response.status_code, 200)
# 模拟用户登出(访问登出接口)
response = self.client.get(reverse('account:logout'))
# 断言:登出响应状态码在[301,302,200]内(重定向或成功)
self.assertIn(response.status_code, [301, 302, 200])
# 登出后再次访问文章管理员编辑页(应无权限)
response = self.client.get(article.get_admin_url())
# 断言:响应状态码在[301,302,200]内(可能重定向到登录页)
self.assertIn(response.status_code, [301, 302, 200])
# 模拟使用错误密码登录(密码不匹配)
response = self.client.post(reverse('account:login'), {
'username': 'user1233', # 正确用户名
'password': 'password123' # 错误密码
})
# 断言:登录响应状态码在[301,302,200]内(登录失败可能重定向或返回表单)
self.assertIn(response.status_code, [301, 302, 200])
# 错误登录后访问文章管理员编辑页(仍无权限)
response = self.client.get(article.get_admin_url())
# 断言:响应状态码在[301,302,200]内(可能重定向到登录页)
self.assertIn(response.status_code, [301, 302, 200])
# 测试邮箱验证码的生成、存储、发送和验证功能
def test_verify_email_code(self):
# 定义测试邮箱地址
to_email = "admin@admin.com"
# 生成随机邮箱验证码(调用工具函数)
code = generate_code()
# 存储验证码(关联邮箱和验证码,用于后续验证)
utils.set_code(to_email, code)
# 发送验证邮件(调用工具函数,将验证码发送到测试邮箱)
utils.send_verify_email(to_email, code)
# 验证:使用正确邮箱和正确验证码
err = utils.verify("admin@admin.com", code)
# 断言验证无错误返回None
self.assertEqual(err, None)
# 验证:使用错误邮箱和正确验证码
err = utils.verify("admin@123.com", code)
# 断言:验证错误,错误类型为字符串(返回错误信息)
self.assertEqual(type(err), str)
# 测试“忘记密码-发送验证码”功能的成功场景
def test_forget_password_email_code_success(self):
# 模拟POST请求提交邮箱访问“发送忘记密码验证码”接口
resp = self.client.post(
path=reverse("account:forget_password_code"), # 反向解析接口URL
data=dict(email="admin@admin.com") # 提交已存在的测试邮箱
)
# 断言响应状态码为200请求处理成功
self.assertEqual(resp.status_code, 200)
# 断言:响应内容为"ok"(表示验证码发送成功)
self.assertEqual(resp.content.decode("utf-8"), "ok")
# 测试“忘记密码-发送验证码”功能的失败场景
def test_forget_password_email_code_fail(self):
# 模拟POST请求不提交邮箱空数据
resp = self.client.post(
path=reverse("account:forget_password_code"),
data=dict() # 空数据
)
# 断言:响应内容为“错误的邮箱”(无邮箱参数,请求失败)
self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱")
# 模拟POST请求提交格式错误的邮箱无效邮箱
resp = self.client.post(
path=reverse("account:forget_password_code"),
data=dict(email="admin@com") # 格式错误的邮箱
)
# 断言:响应内容为“错误的邮箱”(邮箱格式无效,请求失败)
self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱")
# 测试“忘记密码-重置密码”功能的成功场景
def test_forget_password_email_success(self):
# 生成随机验证码
code = generate_code()
# 存储验证码(关联测试用户的邮箱)
utils.set_code(self.blog_user.email, code)
# 构造重置密码的请求数据
data = dict(
new_password1=self.new_test, # 新密码
new_password2=self.new_test, # 新密码确认(与新密码一致)
email=self.blog_user.email, # 测试用户邮箱
code=code, # 正确的验证码
)
# 模拟POST请求提交重置密码数据访问重置密码接口
resp = self.client.post(
path=reverse("account:forget_password"), # 反向解析接口URL
data=data
)
# 断言响应状态码为302重置成功重定向到登录页或结果页
self.assertEqual(resp.status_code, 302)
# 验证:数据库中用户密码是否已更新
blog_user = BlogUser.objects.filter(
email=self.blog_user.email, # 按邮箱查询测试用户
).first() # 获取查询结果的第一个(唯一用户)
# 断言:查询到用户(用户存在)
self.assertNotEqual(blog_user, None)
# 断言用户密码与新密码匹配check_password方法验证哈希密码
self.assertEqual(blog_user.check_password(data["new_password1"]), True)
# 测试“忘记密码-重置密码”功能:邮箱不存在的失败场景
def test_forget_password_email_not_user(self):
# 构造重置密码请求数据(使用不存在的邮箱)
data = dict(
new_password1=self.new_test, # 新密码
new_password2=self.new_test, # 新密码确认
email="123@123.com", # 不存在的邮箱
code="123456", # 任意验证码
)
# 模拟POST请求提交数据访问重置密码接口
>>>>>>> zh_branch
resp = self.client.post(
path=reverse("account:forget_password"),
data=data
)
<<<<<<< HEAD
self.assertEqual(resp.status_code, 200)
def test_forget_password_email_code_error(self):
code = generate_code()
utils.set_code(self.blog_user.email, code)
data = dict(
new_password1=self.new_test,
new_password2=self.new_test,
email=self.blog_user.email,
code="111111",
)
=======
# 断言响应状态码为200请求处理完成但重置失败返回表单页
self.assertEqual(resp.status_code, 200)
# 测试“忘记密码-重置密码”功能:验证码错误的失败场景
def test_forget_password_email_code_error(self):
# 生成正确的验证码并存储(关联测试用户邮箱)
code = generate_code()
utils.set_code(self.blog_user.email, code)
# 构造重置密码请求数据(使用错误的验证码)
data = dict(
new_password1=self.new_test, # 新密码
new_password2=self.new_test, # 新密码确认
email=self.blog_user.email, # 正确的测试用户邮箱
code="111111", # 错误的验证码
)
# 模拟POST请求提交数据访问重置密码接口
>>>>>>> zh_branch
resp = self.client.post(
path=reverse("account:forget_password"),
data=data
)
<<<<<<< HEAD
self.assertEqual(resp.status_code, 200)
=======
# 断言响应状态码为200请求处理完成但验证码错误返回表单页
self.assertEqual(resp.status_code, 200)
>>>>>>> zh_branch
=======
class ArticleTest(TestCase):
"""
文章模型测试类
这个测试类用于测试博客系统的核心功能包括
- 文章创建和验证
- 搜索功能
- 分页功能
- 文件上传
- 管理命令
- 错误页面处理
"""
def setUp(self):
"""
测试初始化方法
在每个测试方法执行前运行用于设置测试环境
"""
self.client = Client() # Django测试客户端用于模拟HTTP请求
self.factory = RequestFactory() # 请求工厂,用于创建请求对象
def test_validate_article(self):
"""
测试文章验证和核心功能
这个测试方法验证博客系统的核心功能
- 用户创建和认证
- 文章创建和关联
- 搜索功能
- 分页功能
- RSS和站点地图
- 管理后台访问
"""
# 获取当前站点域名
site = get_current_site().domain
# 创建测试用户(超级用户)
user = BlogUser.objects.get_or_create(
email="liangliangyy@gmail.com",
username="liangliangyy")[0]
user.set_password("liangliangyy")
user.is_staff = True # 设置为管理员
user.is_superuser = True # 设置为超级用户
user.save()
# 测试用户详情页访问
response = self.client.get(user.get_absolute_url())
self.assertEqual(response.status_code, 200) # 断言返回200状态码
# 测试管理后台页面访问
response = self.client.get('/admin/servermanager/emailsendlog/')
response = self.client.get('admin/admin/logentry/')
# 创建侧边栏测试数据
s = SideBar()
s.sequence = 1
s.name = 'test'
s.content = 'test content'
s.is_enable = True
s.save()
# 创建分类测试数据
category = Category()
category.name = "category"
category.creation_time = timezone.now()
category.last_mod_time = timezone.now()
category.save()
# 创建标签测试数据
tag = Tag()
tag.name = "nicetag"
tag.save()
# 创建文章测试数据
article = Article()
article.title = "nicetitle"
article.body = "nicecontent"
article.author = user
article.category = category
article.type = 'a' # 文章类型
article.status = 'p' # 发布状态
article.save()
# 验证初始标签数量为0
self.assertEqual(0, article.tags.count())
# 添加标签到文章
article.tags.add(tag)
article.save()
# 验证标签数量为1
self.assertEqual(1, article.tags.count())
# 批量创建20篇文章用于测试分页和搜索
for i in range(20):
article = Article()
article.title = "nicetitle" + str(i)
article.body = "nicetitle" + str(i)
article.author = user
article.category = category
article.type = 'a'
article.status = 'p'
article.save()
article.tags.add(tag)
article.save()
# 测试搜索功能如果启用了Elasticsearch
from blog.documents import ELASTICSEARCH_ENABLED
if ELASTICSEARCH_ENABLED:
call_command("build_index") # 构建搜索索引
response = self.client.get('/search', {'q': 'nicetitle'})
self.assertEqual(response.status_code, 200)
# 测试文章详情页访问
response = self.client.get(article.get_absolute_url())
self.assertEqual(response.status_code, 200)
# 测试百度推送通知
from djangoblog.spider_notify import SpiderNotify
SpiderNotify.notify(article.get_absolute_url())
# 测试标签页访问
response = self.client.get(tag.get_absolute_url())
self.assertEqual(response.status_code, 200)
# 测试分类页访问
response = self.client.get(category.get_absolute_url())
self.assertEqual(response.status_code, 200)
# 测试搜索页面
response = self.client.get('/search', {'q': 'django'})
self.assertEqual(response.status_code, 200)
# 测试模板标签函数
s = load_articletags(article)
self.assertIsNotNone(s)
# 用户登录测试
self.client.login(username='liangliangyy', password='liangliangyy')
# 测试文章归档页面
response = self.client.get(reverse('blog:archives'))
self.assertEqual(response.status_code, 200)
# 测试各种分页场景
p = Paginator(Article.objects.all(), settings.PAGINATE_BY)
self.check_pagination(p, '', '') # 基础分页
p = Paginator(Article.objects.filter(tags=tag), settings.PAGINATE_BY)
self.check_pagination(p, '分类标签归档', tag.slug) # 标签分页
p = Paginator(
Article.objects.filter(
author__username='liangliangyy'), settings.PAGINATE_BY)
self.check_pagination(p, '作者文章归档', 'liangliangyy') # 作者分页
p = Paginator(Article.objects.filter(category=category), settings.PAGINATE_BY)
self.check_pagination(p, '分类目录归档', category.slug) # 分类分页
# 测试搜索表单
f = BlogSearchForm()
f.search()
# 测试百度批量推送
from djangoblog.spider_notify import SpiderNotify
SpiderNotify.baidu_notify([article.get_full_url()])
# 测试Gravatar相关功能
from blog.templatetags.blog_tags import gravatar_url, gravatar
u = gravatar_url('liangliangyy@gmail.com')
u = gravatar('liangliangyy@gmail.com')
# 测试友情链接功能
link = Links(
sequence=1,
name="lylinux",
link='https://wwww.lylinux.net')
link.save()
response = self.client.get('/links.html')
self.assertEqual(response.status_code, 200)
# 测试RSS订阅
response = self.client.get('/feed/')
self.assertEqual(response.status_code, 200)
# 测试站点地图
response = self.client.get('/sitemap.xml')
self.assertEqual(response.status_code, 200)
# 测试管理后台操作
self.client.get("/admin/blog/article/1/delete/")
self.client.get('/admin/servermanager/emailsendlog/')
self.client.get('/admin/admin/logentry/')
self.client.get('/admin/admin/logentry/1/change/')
def check_pagination(self, p, type, value):
"""
分页功能测试辅助方法
Args:
p: Paginator分页对象
type: 分页类型用于生成URL
value: 分页参数值
"""
for page in range(1, p.num_pages + 1):
# 加载分页信息
s = load_pagination_info(p.page(page), type, value)
self.assertIsNotNone(s)
# 测试上一页链接
if s['previous_url']:
response = self.client.get(s['previous_url'])
self.assertEqual(response.status_code, 200)
# 测试下一页链接
if s['next_url']:
response = self.client.get(s['next_url'])
self.assertEqual(response.status_code, 200)
def test_image(self):
"""
图片上传和头像处理测试
"""
import requests
# 下载测试图片
rsp = requests.get(
'https://www.python.org/static/img/python-logo.png')
imagepath = os.path.join(settings.BASE_DIR, 'python.png')
with open(imagepath, 'wb') as file:
file.write(rsp.content)
# 测试未授权上传应该返回403
rsp = self.client.post('/upload')
self.assertEqual(rsp.status_code, 403)
# 生成签名用于授权上传
sign = get_sha256(get_sha256(settings.SECRET_KEY))
with open(imagepath, 'rb') as file:
imgfile = SimpleUploadedFile(
'python.png', file.read(), content_type='image/jpg')
form_data = {'python.png': imgfile}
# 测试授权上传
rsp = self.client.post(
'/upload?sign=' + sign, form_data, follow=True)
self.assertEqual(rsp.status_code, 200)
# 清理测试文件
os.remove(imagepath)
# 测试工具函数
from djangoblog.utils import save_user_avatar, send_email
send_email(['qq@qq.com'], 'testTitle', 'testContent')
save_user_avatar(
'https://www.python.org/static/img/python-logo.png')
def test_errorpage(self):
"""测试404错误页面"""
rsp = self.client.get('/eee')
self.assertEqual(rsp.status_code, 404)
def test_commands(self):
"""
测试Django管理命令
验证系统提供的各种管理命令是否能正常执行
"""
# 创建测试用户
user = BlogUser.objects.get_or_create(
email="liangliangyy@gmail.com",
username="liangliangyy")[0]
user.set_password("liangliangyy")
user.is_staff = True
user.is_superuser = True
user.save()
# 创建OAuth配置
c = OAuthConfig()
c.type = 'qq'
c.appkey = 'appkey'
c.appsecret = 'appsecret'
c.save()
# 创建OAuth用户
u = OAuthUser()
u.type = 'qq'
u.openid = 'openid'
u.user = user
u.picture = static("/blog/img/avatar.png")
u.metadata = '''
{
"figureurl": "https://qzapp.qlogo.cn/qzapp/101513904/C740E30B4113EAA80E0D9918ABC78E82/30"
}'''
u.save()
u = OAuthUser()
u.type = 'qq'
u.openid = 'openid1'
u.picture = 'https://qzapp.qlogo.cn/qzapp/101513904/C740E30B4113EAA80E0D9918ABC78E82/30'
u.metadata = '''
{
"figureurl": "https://qzapp.qlogo.cn/qzapp/101513904/C740E30B4113EAA80E0D9918ABC78E82/30"
}'''
u.save()
# 测试各种管理命令
from blog.documents import ELASTICSEARCH_ENABLED
if ELASTICSEARCH_ENABLED:
call_command("build_index") # 构建搜索索引
call_command("ping_baidu", "all") # 百度推送
call_command("create_testdata") # 创建测试数据
call_command("clear_cache") # 清理缓存
call_command("sync_user_avatar") # 同步用户头像
call_command("build_search_words") # 构建搜索词
>>>>>>> hyt_branch

@ -0,0 +1,253 @@
<<<<<<< HEAD
<<<<<<< HEAD
<<<<<<< HEAD
"""djangoblog URL Configuration
项目URL路由总配置文件定义所有URL与视图/应用的映射关系
核心作用是将用户访问的URL地址分发到对应的应用或视图函数处理
"""
# 导入项目配置,用于获取静态资源、媒体文件路径等
from django.conf import settings
# 导入国际化URL配置工具支持多语言URL前缀如/en/、/zh-hans/
from django.conf.urls.i18n import i18n_patterns
# 导入静态资源URL配置工具用于开发环境下提供静态文件访问
from django.conf.urls.static import static
# 导入站点地图视图用于生成sitemap.xml
from django.contrib.sitemaps.views import sitemap
# 导入URL路径配置工具path用于固定路径re_path支持正则匹配include用于包含子应用URL
from django.urls import path, include, re_path
# 导入Haystack搜索视图工厂用于自定义搜索视图
from haystack.views import search_view_factory
# 导入自定义视图和配置博客搜索视图、自定义Admin站点、ElasticSearch搜索表单
from blog.views import EsSearchView
from djangoblog.admin_site import admin_site
from djangoblog.elasticsearch_backend import ElasticSearchModelSearchForm
# 导入RSS订阅Feed和站点地图类
from djangoblog.feeds import DjangoBlogFeed
from djangoblog.sitemap import (ArticleSiteMap, CategorySiteMap,
StaticViewSitemap, TagSiteMap, UserSiteMap)
# -------------------------- 站点地图配置 --------------------------
# 定义站点地图字典关联不同类型页面的Sitemap类用于生成sitemap.xml
sitemaps = {
'blog': ArticleSiteMap, # 文章页面的站点地图
'Category': CategorySiteMap, # 分类页面的站点地图
'Tag': TagSiteMap, # 标签页面的站点地图
'User': UserSiteMap, # 用户主页的站点地图
'static': StaticViewSitemap # 静态页面(如首页)的站点地图
}
# -------------------------- 自定义错误页面配置 --------------------------
# 配置404页面不存在错误对应的处理视图
handler404 = 'blog.views.page_not_found_view'
# 配置500服务器内部错误错误对应的处理视图
handler500 = 'blog.views.server_error_view'
# 配置403权限不足错误对应的处理视图
handle403 = 'blog.views.permission_denied_view'
# -------------------------- 基础URL配置 --------------------------
# 非国际化URL列表不随语言切换变化的URL
urlpatterns = [
# 国际化切换入口:提供语言选择功能(如切换中英文)
path('i18n/', include('django.conf.urls.i18n')),
]
# -------------------------- 国际化URL配置 --------------------------
# 国际化URL列表会自动添加语言前缀如/zh-hans/admin/、/en/admin/
# prefix_default_language=False默认语言如中文不添加语言前缀保持URL简洁
urlpatterns += i18n_patterns(
# 自定义Admin后台URL使用项目自定义的admin_site非Django默认Admin
re_path(r'^admin/', admin_site.urls),
# 博客核心功能URL包含文章列表、详情等命名空间为'blog'
re_path(r'', include('blog.urls', namespace='blog')),
# Markdown编辑器URL集成mdeditor插件的路由
re_path(r'mdeditor/', include('mdeditor.urls')),
# 评论功能URL包含评论提交、列表等命名空间为'comment'
re_path(r'', include('comments.urls', namespace='comment')),
# 用户账户功能URL包含登录、注册、个人中心等命名空间为'account'
re_path(r'', include('accounts.urls', namespace='account')),
# 第三方登录URL包含GitHub、微信等登录命名空间为'oauth'
re_path(r'', include('oauth.urls', namespace='oauth')),
# 站点地图URL生成sitemap.xml供搜索引擎抓取
re_path(r'^sitemap\.xml$', sitemap, {'sitemaps': sitemaps},
name='django.contrib.sitemaps.views.sitemap'),
# RSS订阅URL提供两种路径/feed/和/rss/均指向DjangoBlogFeed
re_path(r'^feed/$', DjangoBlogFeed()),
re_path(r'^rss/$', DjangoBlogFeed()),
# 搜索功能URL使用ElasticSearch搜索视图和表单命名空间为'search'
re_path('^search', search_view_factory(
view_class=EsSearchView,
form_class=ElasticSearchModelSearchForm
), name='search'),
# 服务器管理功能URL包含系统监控等命名空间为'servermanager'
re_path(r'', include('servermanager.urls', namespace='servermanager')),
# 位置追踪功能URL集成owntracks的路由命名空间为'owntracks'
re_path(r'', include('owntracks.urls', namespace='owntracks')),
prefix_default_language=False # 默认语言URL不添加语言前缀
)
# -------------------------- 静态资源与媒体文件URL配置 --------------------------
# 开发环境下添加静态文件URL映射生产环境由Nginx/Apache处理
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
# DEBUG模式下开发环境添加媒体文件用户上传文件的URL映射
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL,
document_root=settings.MEDIA_ROOT)
=======
from django.urls import path
from django.urls import re_path
from . import views
from .forms import LoginForm
app_name = "accounts"
urlpatterns = [re_path(r'^login/$',
views.LoginView.as_view(success_url='/'),
name='login',
kwargs={'authentication_form': LoginForm}),
re_path(r'^register/$',
views.RegisterView.as_view(success_url="/"),
name='register'),
re_path(r'^logout/$',
views.LogoutView.as_view(),
name='logout'),
path(r'account/result.html',
views.account_result,
name='result'),
re_path(r'^forget_password/$',
views.ForgetPasswordView.as_view(),
name='forget_password'),
re_path(r'^forget_password_code/$',
views.ForgetPasswordEmailCode.as_view(),
name='forget_password_code'),
]
>>>>>>> sh_branch
=======
from django.urls import path
# 导入当前应用comments的views模块用于关联视图函数/类
from . import views
# 定义当前应用的命名空间为"comments"避免URL名称冲突
app_name = "comments"
# 定义URL路由列表存储URL规则与视图的映射关系
urlpatterns = [
path(
'article/<int:article_id>/postcomment', # URL路径包含文章ID整数类型的动态路径
views.CommentPostView.as_view(), # 关联的视图类调用CommentPostView的as_view()方法生成视图函数
name='postcomment'), # 给该URL命名为"postcomment",用于反向解析
]
>>>>>>> zh_branch
=======
from django.urls import path
from django.views.decorators.cache import cache_page
from . import views
# 应用命名空间用于URL反向解析时区分不同应用的URL
app_name = "blog"
# URL模式配置定义了博客应用的所有URL路由
urlpatterns = [
# 首页路由
path(
r'', # 空路径匹配根URL/ 或 /blog/
views.IndexView.as_view(), # 使用类视图处理首页
name='index' # URL名称用于反向解析
),
# 首页分页路由
path(
r'page/<int:page>/', # 带页码的路径(如:/page/2/
views.IndexView.as_view(), # 使用相同的类视图,但会处理分页
name='index_page' # URL名称
),
# 文章详情页路由SEO友好URL
path(
r'article/<int:year>/<int:month>/<int:day>/<int:article_id>.html', # 包含年月日和文章ID的URL
views.ArticleDetailView.as_view(), # 文章详情类视图
name='detailbyid' # URL名称
),
# 分类详情页路由
path(
r'category/<slug:category_name>.html', # 使用分类名称的slug格式
views.CategoryDetailView.as_view(), # 分类详情类视图
name='category_detail' # URL名称
),
# 分类详情分页路由
path(
r'category/<slug:category_name>/<int:page>.html', # 带页码的分类URL
views.CategoryDetailView.as_view(), # 相同的类视图处理分页
name='category_detail_page' # URL名称
),
# 作者详情页路由
path(
r'author/<author_name>.html', # 使用作者名称的URL
views.AuthorDetailView.as_view(), # 作者详情类视图
name='author_detail' # URL名称
),
# 作者详情分页路由
path(
r'author/<author_name>/<int:page>.html', # 带页码的作者URL
views.AuthorDetailView.as_view(), # 相同的类视图处理分页
name='author_detail_page' # URL名称
),
# 标签详情页路由
path(
r'tag/<slug:tag_name>.html', # 使用标签名称的slug格式
views.TagDetailView.as_view(), # 标签详情类视图
name='tag_detail' # URL名称
),
# 标签详情分页路由
path(
r'tag/<slug:tag_name>/<int:page>.html', # 带页码的标签URL
views.TagDetailView.as_view(), # 相同的类视图处理分页
name='tag_detail_page' # URL名称
),
# 文章归档页路由(带缓存)
path(
'archives.html', # 归档页面URL
cache_page(60 * 60)(views.ArchivesView.as_view()), # 使用缓存装饰器缓存1小时
name='archives' # URL名称
),
# 友情链接页面路由
path(
'links.html', # 友情链接页面URL
views.LinkListView.as_view(), # 链接列表类视图
name='links' # URL名称
),
# 文件上传路由
path(
r'upload', # 文件上传端点
views.fileupload, # 使用函数视图处理文件上传
name='upload' # URL名称
),
# 缓存清理路由
path(
r'clean', # 缓存清理端点
views.clean_cache_view, # 使用函数视图处理缓存清理
name='clean' # URL名称
),
]
>>>>>>> hyt_branch

@ -0,0 +1,26 @@
from django.contrib.auth import get_user_model
from django.contrib.auth.backends import ModelBackend
class EmailOrUsernameModelBackend(ModelBackend):
"""
允许使用用户名或邮箱登录
"""
def authenticate(self, request, username=None, password=None, **kwargs):
if '@' in username:
kwargs = {'email': username}
else:
kwargs = {'username': username}
try:
user = get_user_model().objects.get(**kwargs)
if user.check_password(password):
return user
except get_user_model().DoesNotExist:
return None
def get_user(self, username):
try:
return get_user_model().objects.get(pk=username)
except get_user_model().DoesNotExist:
return None

@ -0,0 +1,468 @@
<<<<<<< HEAD
<<<<<<< HEAD
#!/usr/bin/env python
# encoding: utf-8
# 导入必要模块日志、文件操作、随机数生成、加密、HTTP请求等
import logging
import os
import random
import string
import uuid
from hashlib import sha256
# 导入第三方库HTML过滤、Markdown转换、HTTP请求
import bleach
import markdown
import requests
# 导入Django核心模块配置、缓存、站点模型、静态文件工具
from django.conf import settings
from django.contrib.sites.models import Site
from django.core.cache import cache
from django.templatetags.static import static
# 创建当前模块的日志记录器
logger = logging.getLogger(__name__)
def get_max_articleid_commentid():
"""
获取最大的文章ID和评论ID
用于系统统计或数据同步场景快速获取最新数据的ID边界
"""
# 延迟导入模型(避免循环导入问题)
from blog.models import Article
from comments.models import Comment
# 返回最新文章和评论的主键ID
return (Article.objects.latest().pk, Comment.objects.latest().pk)
def get_sha256(str):
"""
对字符串进行SHA256加密
用于密码加密数据校验等场景如生成唯一标识
"""
m = sha256(str.encode('utf-8')) # 编码为UTF-8后加密
return m.hexdigest() # 返回十六进制加密结果
def cache_decorator(expiration=3 * 60):
"""
缓存装饰器为函数添加缓存功能减少重复计算或数据库查询
默认缓存时间为3分钟180可通过参数调整
Args:
expiration: 缓存过期时间
"""
def wrapper(func):
def news(*args, **kwargs):
# 尝试从请求对象中获取缓存键(适用于视图函数)
try:
view = args[0]
key = view.get_cache_key()
except:
key = None # 非视图函数则生成自定义键
# 生成自定义缓存键(基于函数和参数的唯一标识)
if not key:
unique_str = repr((func, args, kwargs)) # 序列化函数和参数
m = sha256(unique_str.encode('utf-8'))
key = m.hexdigest() # 生成唯一哈希键
# 从缓存获取数据
value = cache.get(key)
if value is not None:
# 处理空值标记避免缓存None导致的重复计算
if str(value) == '__default_cache_value__':
return None
else:
return value # 返回缓存数据
else:
# 缓存未命中,执行原函数并缓存结果
logger.debug(f'cache_decorator set cache:{func.__name__} key:{key}')
value = func(*args, **kwargs)
# 缓存空值时用特殊标记,避免缓存穿透
if value is None:
cache.set(key, '__default_cache_value__', expiration)
else:
cache.set(key, value, expiration)
return value
return news
return wrapper
def expire_view_cache(path, servername, serverport, key_prefix=None):
'''
主动刷新视图缓存删除指定URL路径的缓存
用于数据更新后同步清理缓存确保用户看到最新内容
Args:
path: URL路径'/article/1/'
servername: 服务器域名/主机名
serverport: 服务器端口
key_prefix: 缓存键前缀
Returns:
bool: 缓存是否成功删除
'''
from django.http import HttpRequest
from django.utils.cache import get_cache_key
# 构造模拟请求对象(用于生成缓存键)
request = HttpRequest()
request.META = {'SERVER_NAME': servername, 'SERVER_PORT': serverport}
request.path = path
# 获取缓存键并删除缓存
key = get_cache_key(request, key_prefix=key_prefix, cache=cache)
if key:
logger.info(f'expire_view_cache:get key:{path}')
if cache.get(key):
cache.delete(key)
return True
return False
@cache_decorator()
def get_current_site():
"""
获取当前站点信息缓存装饰器确保高效获取
基于Django的sites框架用于生成绝对URL等场景
"""
site = Site.objects.get_current()
return site
class CommonMarkdown:
"""
Markdown转换工具类将Markdown文本转换为HTML并支持生成目录TOC
集成代码高亮表格等扩展功能
"""
@staticmethod
def _convert_markdown(value):
"""内部转换方法执行Markdown到HTML的转换返回内容和目录"""
md = markdown.Markdown(
extensions=[
'extra', # 基础扩展(表格、脚注等)
'codehilite', # 代码高亮
'toc', # 目录生成
'tables', # 表格支持
]
)
body = md.convert(value) # 转换正文为HTML
toc = md.toc # 提取目录
return body, toc
@staticmethod
def get_markdown_with_toc(value):
"""获取带目录的HTML内容"""
body, toc = CommonMarkdown._convert_markdown(value)
return body, toc
@staticmethod
def get_markdown(value):
"""仅获取转换后的HTML正文不含目录"""
body, toc = CommonMarkdown._convert_markdown(value)
return body
def send_email(emailto, title, content):
"""
发送邮件的封装函数通过信号机制发送邮件解耦邮件发送逻辑
实际发送由信号接收者处理如调用Django邮件后端
"""
from djangoblog.blog_signals import send_email_signal
# 发送信号,传递邮件参数
send_email_signal.send(
send_email.__class__,
emailto=emailto,
title=title,
content=content)
def generate_code() -> str:
"""生成6位数字随机验证码用于邮箱验证、登录等场景"""
return ''.join(random.sample(string.digits, 6)) # 从0-9中随机选择6个数字
def parse_dict_to_url(dict):
"""
将字典转换为URL查询参数字符串{'a':1,'b':2} 'a=1&b=2'
自动对键值进行URL编码支持特殊字符
Args:
dict: 键值对字典
Returns:
str: URL查询参数字符串
"""
from urllib.parse import quote
return '&'.join([
f'{quote(k, safe="/")}={quote(v, safe="/")}'
for k, v in dict.items()
])
def get_blog_setting():
"""
获取博客系统设置单例模式并缓存结果
包含站点名称描述SEO配置等核心设置
Returns:
BlogSettings对象系统设置实例
"""
# 先从缓存获取,减少数据库查询
value = cache.get('get_blog_setting')
if value:
return value
else:
from blog.models import BlogSettings
# 若不存在设置记录,创建默认配置
if not BlogSettings.objects.count():
setting = BlogSettings()
setting.site_name = 'djangoblog'
setting.site_description = '基于Django的博客系统'
setting.site_seo_description = '基于Django的博客系统'
setting.site_keywords = 'Django,Python'
setting.article_sub_length = 300
setting.sidebar_article_count = 10
setting.sidebar_comment_count = 5
setting.show_google_adsense = False
setting.open_site_comment = True
setting.analytics_code = ''
setting.beian_code = ''
setting.show_gongan_code = False
setting.comment_need_review = False
setting.save()
# 获取设置并缓存
value = BlogSettings.objects.first()
logger.info('set cache get_blog_setting')
cache.set('get_blog_setting', value)
return value
def save_user_avatar(url):
'''
保存用户头像到本地静态目录并返回访问URL
用于处理第三方登录如GitHub的头像保存
Args:
url: 头像的远程URL
Returns:
str: 本地头像的静态文件URL默认返回系统默认头像
'''
logger.info(url)
try:
# 定义本地保存路径static/avatar目录
basedir = os.path.join(settings.STATICFILES, 'avatar')
# 下载头像图片
rsp = requests.get(url, timeout=2)
if rsp.status_code == 200:
# 创建目录(若不存在)
if not os.path.exists(basedir):
os.makedirs(basedir)
# 验证文件类型并确定扩展名
image_extensions = ['.jpg', '.png', 'jpeg', '.gif']
isimage = len([i for i in image_extensions if url.endswith(i)]) > 0
ext = os.path.splitext(url)[1] if isimage else '.jpg'
# 生成唯一文件名UUID避免冲突
save_filename = str(uuid.uuid4().hex) + ext
logger.info(f'保存用户头像:{basedir}{save_filename}')
# 写入文件
with open(os.path.join(basedir, save_filename), 'wb+') as file:
file.write(rsp.content)
# 返回静态文件URL
return static('avatar/' + save_filename)
except Exception as e:
logger.error(e)
# 失败时返回默认头像
return static('blog/img/avatar.png')
def delete_sidebar_cache():
"""
删除侧边栏缓存当侧边栏数据如链接文章列表更新时调用
确保用户看到最新的侧边栏内容
"""
from blog.models import LinkShowType
# 生成所有侧边栏缓存键(基于链接展示类型)
keys = ["sidebar" + x for x in LinkShowType.values]
for k in keys:
logger.info(f'delete sidebar key:{k}')
cache.delete(k)
def delete_view_cache(prefix, keys):
"""
删除模板片段缓存用于删除指定前缀和参数的模板缓存
如文章详情页的评论区缓存
Args:
prefix: 缓存前缀模板中定义
keys: 缓存键参数列表
"""
from django.core.cache.utils import make_template_fragment_key
key = make_template_fragment_key(prefix, keys)
cache.delete(key)
def get_resource_url():
"""
获取静态资源基础URL
优先使用settings中的STATIC_URL否则基于当前站点域名生成
Returns:
str: 静态资源URL前缀'http://example.com/static/'
"""
if settings.STATIC_URL:
return settings.STATIC_URL
else:
site = get_current_site()
return f'http://{site.domain}/static/'
# HTML过滤配置限制允许的标签和属性防止XSS攻击
ALLOWED_TAGS = ['a', 'abbr', 'acronym', 'b', 'blockquote', 'code', 'em', 'i',
'li', 'ol', 'pre', 'strong', 'ul', 'h1', 'h2', 'p']
ALLOWED_ATTRIBUTES = {
'a': ['href', 'title'],
'abbr': ['title'],
'acronym': ['title']
}
def sanitize_html(html):
"""
净化HTML内容仅保留允许的标签和属性过滤恶意代码
用于处理用户输入的HTML如评论文章内容防止XSS攻击
Args:
html: 原始HTML字符串
Returns:
str: 净化后的安全HTML
"""
return bleach.clean(
html,
tags=ALLOWED_TAGS,
attributes=ALLOWED_ATTRIBUTES
)
=======
import typing
from datetime import timedelta
from django.core.cache import cache
from django.utils.translation import gettext
from django.utils.translation import gettext_lazy as _
from djangoblog.utils import send_email
_code_ttl = timedelta(minutes=5)
def send_verify_email(to_mail: str, code: str, subject: str = _("Verify Email")):
"""发送重设密码验证码
Args:
to_mail: 接受邮箱
subject: 邮件主题
code: 验证码
"""
html_content = _(
"You are resetting the password, the verification code is%(code)s, valid within 5 minutes, please keep it "
"properly") % {'code': code}
send_email([to_mail], subject, html_content)
def verify(email: str, code: str) -> typing.Optional[str]:
"""验证code是否有效
Args:
email: 请求邮箱
code: 验证码
Return:
如果有错误就返回错误str
Node:
这里的错误处理不太合理应该采用raise抛出
否测调用方也需要对error进行处理
"""
cache_code = get_code(email)
if cache_code != code:
return gettext("Verification code error")
def set_code(email: str, code: str):
"""设置code"""
cache.set(email, code, _code_ttl.seconds)
def get_code(email: str) -> typing.Optional[str]:
"""获取code"""
return cache.get(email)
>>>>>>> sh_branch
=======
# 导入logging模块用于记录日志如异常信息
import logging
# 导入国际化翻译函数,用于生成多语言邮件内容
from django.utils.translation import gettext_lazy as _
# 导入项目工具函数,获取当前站点域名
from djangoblog.utils import get_current_site
# 导入项目工具函数,用于发送邮件
from djangoblog.utils import send_email
# 创建当前模块的日志记录器,用于记录该函数的运行日志
logger = logging.getLogger(__name__)
# 定义发送评论相关邮件的函数,接收评论对象作为参数
def send_comment_email(comment):
# 获取当前站点的域名(用于拼接文章链接)
site = get_current_site().domain
# 定义邮件主题(多语言翻译,如中文为“感谢您的评论”)
subject = _('Thanks for your comment')
# 拼接评论所属文章的完整URLHTTPS协议 + 域名 + 文章相对路径)
article_url = f"https://{site}{comment.article.get_absolute_url()}"
# 定义邮件HTML内容多语言模板包含感谢语、文章链接、链接提示
# 使用字符串格式化,替换{article_url}和{article_title}为实际值
html_content = _("""<p>Thank you very much for your comments on this site</p>
You can visit <a href="%(article_url)s" rel="bookmark">%(article_title)s</a>
to review your comments,
Thank you again!
<br />
If the link above cannot be opened, please copy this link to your browser.
%(article_url)s""") % {'article_url': article_url, 'article_title': comment.article.title}
tomail = comment.author.email
# 调用send_email函数发送邮件收件人列表、主题、HTML内容
send_email([tomail], subject, html_content)
# 尝试给父评论作者发送“评论被回复”的邮件(若当前评论是回复)
try:
# 判断当前评论是否有父评论(即是否为回复)
if comment.parent_comment:
# 定义回复通知的HTML邮件内容多语言模板
# 包含父评论所属文章链接、父评论内容、查看提示
html_content = _("""Your comment on <a href="%(article_url)s" rel="bookmark">%(article_title)s</a><br/> has
received a reply. <br/> %(comment_body)s
<br/>
go check it out!
<br/>
If the link above cannot be opened, please copy this link to your browser.
%(article_url)s
""") % {'article_url': article_url, 'article_title': comment.article.title,
'comment_body': comment.parent_comment.body}
# 获取父评论作者的邮箱(回复通知的收件人)
tomail = comment.parent_comment.author.email
# 发送回复通知邮件
send_email([tomail], subject, html_content)
# 捕获所有异常,避免发送失败影响主流程
except Exception as e:
logger.error(e)
>>>>>>> zh_branch

@ -0,0 +1,807 @@
<<<<<<< HEAD
<<<<<<< HEAD
import logging
from django.utils.translation import gettext_lazy as _
from django.conf import settings
from django.contrib import auth
from django.contrib.auth import REDIRECT_FIELD_NAME
from django.contrib.auth import get_user_model
from django.contrib.auth import logout
from django.contrib.auth.forms import AuthenticationForm
from django.contrib.auth.hashers import make_password
from django.http import HttpResponseRedirect, HttpResponseForbidden
from django.http.request import HttpRequest
from django.http.response import HttpResponse
from django.shortcuts import get_object_or_404
from django.shortcuts import render
from django.urls import reverse
from django.utils.decorators import method_decorator
from django.utils.http import url_has_allowed_host_and_scheme
from django.views import View
from django.views.decorators.cache import never_cache
from django.views.decorators.csrf import csrf_protect
from django.views.decorators.debug import sensitive_post_parameters
from django.views.generic import FormView, RedirectView
from djangoblog.utils import send_email, get_sha256, get_current_site, generate_code, delete_sidebar_cache
from . import utils
from .forms import RegisterForm, LoginForm, ForgetPasswordForm, ForgetPasswordCodeForm
from .models import BlogUser
=======
import logging
import os
import uuid
from django.conf import settings
from django.core.paginator import Paginator
from django.http import HttpResponse, HttpResponseForbidden
from django.shortcuts import get_object_or_404
from django.shortcuts import render
from django.templatetags.static import static
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from django.views.decorators.csrf import csrf_exempt
from django.views.generic.detail import DetailView
from django.views.generic.list import ListView
from haystack.views import SearchView
from blog.models import Article, Category, LinkShowType, Links, Tag
from comments.forms import CommentForm
from djangoblog.plugin_manage import hooks
from djangoblog.plugin_manage.hook_constants import ARTICLE_CONTENT_HOOK_NAME
from djangoblog.utils import cache, get_blog_setting, get_sha256
>>>>>>> hyt_branch
logger = logging.getLogger(__name__)
<<<<<<< HEAD
# Create your views here.
class RegisterView(FormView):
form_class = RegisterForm
template_name = 'account/registration_form.html'
@method_decorator(csrf_protect)
def dispatch(self, *args, **kwargs):
return super(RegisterView, self).dispatch(*args, **kwargs)
def form_valid(self, form):
if form.is_valid():
user = form.save(False)
user.is_active = False
user.source = 'Register'
user.save(True)
site = get_current_site().domain
sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
if settings.DEBUG:
site = '127.0.0.1:8000'
path = reverse('account:result')
url = "http://{site}{path}?type=validation&id={id}&sign={sign}".format(
site=site, path=path, id=user.id, sign=sign)
content = """
<p>请点击下面链接验证您的邮箱</p>
<a href="{url}" rel="bookmark">{url}</a>
再次感谢您
<br />
如果上面链接无法打开请将此链接复制至浏览器
{url}
""".format(url=url)
send_email(
emailto=[
user.email,
],
title='验证您的电子邮箱',
content=content)
url = reverse('accounts:result') + \
'?type=register&id=' + str(user.id)
return HttpResponseRedirect(url)
else:
return self.render_to_response({
'form': form
})
class LogoutView(RedirectView):
url = '/login/'
@method_decorator(never_cache)
def dispatch(self, request, *args, **kwargs):
return super(LogoutView, self).dispatch(request, *args, **kwargs)
def get(self, request, *args, **kwargs):
logout(request)
delete_sidebar_cache()
return super(LogoutView, self).get(request, *args, **kwargs)
class LoginView(FormView):
form_class = LoginForm
template_name = 'account/login.html'
success_url = '/'
redirect_field_name = REDIRECT_FIELD_NAME
login_ttl = 2626560 # 一个月的时间
@method_decorator(sensitive_post_parameters('password'))
@method_decorator(csrf_protect)
@method_decorator(never_cache)
def dispatch(self, request, *args, **kwargs):
return super(LoginView, self).dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
redirect_to = self.request.GET.get(self.redirect_field_name)
if redirect_to is None:
redirect_to = '/'
kwargs['redirect_to'] = redirect_to
return super(LoginView, self).get_context_data(**kwargs)
def form_valid(self, form):
form = AuthenticationForm(data=self.request.POST, request=self.request)
if form.is_valid():
delete_sidebar_cache()
logger.info(self.redirect_field_name)
auth.login(self.request, form.get_user())
if self.request.POST.get("remember"):
self.request.session.set_expiry(self.login_ttl)
return super(LoginView, self).form_valid(form)
# return HttpResponseRedirect('/')
else:
return self.render_to_response({
'form': form
})
def get_success_url(self):
redirect_to = self.request.POST.get(self.redirect_field_name)
if not url_has_allowed_host_and_scheme(
url=redirect_to, allowed_hosts=[
self.request.get_host()]):
redirect_to = self.success_url
return redirect_to
def account_result(request):
type = request.GET.get('type')
id = request.GET.get('id')
user = get_object_or_404(get_user_model(), id=id)
logger.info(type)
if user.is_active:
return HttpResponseRedirect('/')
if type and type in ['register', 'validation']:
if type == 'register':
content = '''
恭喜您注册成功一封验证邮件已经发送到您的邮箱请验证您的邮箱后登录本站
'''
title = '注册成功'
else:
c_sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
sign = request.GET.get('sign')
if sign != c_sign:
return HttpResponseForbidden()
user.is_active = True
user.save()
content = '''
恭喜您已经成功的完成邮箱验证您现在可以使用您的账号来登录本站
'''
title = '验证成功'
return render(request, 'account/result.html', {
'title': title,
'content': content
})
else:
return HttpResponseRedirect('/')
class ForgetPasswordView(FormView):
form_class = ForgetPasswordForm
template_name = 'account/forget_password.html'
def form_valid(self, form):
if form.is_valid():
blog_user = BlogUser.objects.filter(email=form.cleaned_data.get("email")).get()
blog_user.password = make_password(form.cleaned_data["new_password2"])
blog_user.save()
return HttpResponseRedirect('/login/')
else:
return self.render_to_response({'form': form})
class ForgetPasswordEmailCode(View):
def post(self, request: HttpRequest):
form = ForgetPasswordCodeForm(request.POST)
if not form.is_valid():
return HttpResponse("错误的邮箱")
to_email = form.cleaned_data["email"]
code = generate_code()
utils.send_verify_email(to_email, code)
utils.set_code(to_email, code)
return HttpResponse("ok")
=======
from django.core.exceptions import ValidationError
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404
# 导入方法装饰器工具,用于装饰类中的方法
from django.utils.decorators import method_decorator
# 导入CSRF保护装饰器防止跨站请求伪造
from django.views.decorators.csrf import csrf_protect
# 导入表单视图基类,用于处理表单提交逻辑
from django.views.generic.edit import FormView
# 导入用户模型,用于获取评论作者信息
from accounts.models import BlogUser
# 导入文章模型,用于关联评论所属文章
from blog.models import Article
# 导入评论表单类,用于处理评论提交数据
from .forms import CommentForm
# 导入评论模型,用于创建和保存评论
from .models import Comment
# 定义评论提交视图类继承自FormView表单处理基类
class CommentPostView(FormView):
form_class = CommentForm # 指定使用的表单类为CommentForm
template_name = 'blog/article_detail.html' # 指定表单验证失败时渲染的模板
# 使用CSRF保护装饰器装饰dispatch方法确保表单提交安全
@method_decorator(csrf_protect)
def dispatch(self, *args, **kwargs):
# 调用父类的dispatch方法处理请求分发
return super(CommentPostView, self).dispatch(*args, **kwargs)
# 处理GET请求重定向到文章详情页的评论区
def get(self, request, *args, **kwargs):
# 从URL参数中获取文章ID
article_id = self.kwargs['article_id']
# 获取对应的文章对象不存在则返回404
article = get_object_or_404(Article, pk=article_id)
# 获取文章详情页的URL
url = article.get_absolute_url()
# 重定向到文章详情页的评论区(通过锚点#comments定位
return HttpResponseRedirect(url + "#comments")
# 处理表单验证失败的逻辑
def form_invalid(self, form):
# 从URL参数中获取文章ID
article_id = self.kwargs['article_id']
# 获取对应的文章对象
article = get_object_or_404(Article, pk=article_id)
# 渲染文章详情页模板,传递错误的表单和文章对象(用于显示错误信息)
return self.render_to_response({
'form': form,
'article': article
})
# 处理表单验证成功后的逻辑
def form_valid(self, form):
"""提交的数据验证合法后的逻辑"""
# 获取当前登录用户
user = self.request.user
# 根据用户ID获取对应的用户对象评论作者
author = BlogUser.objects.get(pk=user.pk)
# 从URL参数中获取文章ID
article_id = self.kwargs['article_id']
# 获取对应的文章对象
article = get_object_or_404(Article, pk=article_id)
# 检查文章是否允许评论:若文章评论状态为关闭或文章状态为草稿,则抛出验证错误
if article.comment_status == 'c' or article.status == 'c':
raise ValidationError("该文章评论已关闭.")
# 保存表单数据但不提交到数据库(获取评论对象)
comment = form.save(False)
# 关联评论到对应的文章
comment.article = article
# 导入工具函数,获取博客设置
from djangoblog.utils import get_blog_setting
settings = get_blog_setting()
# 若博客设置为评论无需审核,则直接启用评论
if not settings.comment_need_review:
comment.is_enable = True
# 设置评论的作者
comment.author = author
# 处理回复功能若存在父评论ID则关联到父评论
if form.cleaned_data['parent_comment_id']:
# 根据父评论ID获取父评论对象
parent_comment = Comment.objects.get(
pk=form.cleaned_data['parent_comment_id'])
# 设置当前评论的父评论
comment.parent_comment = parent_comment
# 保存评论到数据库(执行真正的保存操作)
comment.save(True)
# 重定向到文章详情页的当前评论位置(通过锚点#div-comment-{评论ID}定位)
return HttpResponseRedirect(
"%s#div-comment-%d" %
(article.get_absolute_url(), comment.pk))
>>>>>>> zh_branch
=======
class ArticleListView(ListView):
"""
文章列表基类视图
提供通用的文章列表功能和缓存机制
所有文章列表视图都应该继承此类
"""
# template_name属性用于指定使用哪个模板进行渲染
template_name = 'blog/article_index.html'
# context_object_name属性用于给上下文变量取名在模板中使用该名字
context_object_name = 'article_list'
# 页面类型,分类目录或标签列表等
page_type = ''
paginate_by = settings.PAGINATE_BY # 每页显示的文章数量
page_kwarg = 'page' # URL中页码参数的名称
link_type = LinkShowType.L # 友情链接显示类型
def get_view_cache_key(self):
"""获取视图缓存键 - 需要子类实现"""
return self.request.get['pages']
@property
def page_number(self):
"""获取当前页码"""
page_kwarg = self.page_kwarg
page = self.kwargs.get(
page_kwarg) or self.request.GET.get(page_kwarg) or 1
return page
def get_queryset_cache_key(self):
"""
获取查询集缓存键
子类必须重写此方法
"""
raise NotImplementedError()
def get_queryset_data(self):
"""
获取查询集数据
子类必须重写此方法
"""
raise NotImplementedError()
def get_queryset_from_cache(self, cache_key):
"""
从缓存获取查询集数据
Args:
cache_key: 缓存键
Returns:
QuerySet: 文章查询集
"""
value = cache.get(cache_key)
if value:
logger.info('get view cache.key:{key}'.format(key=cache_key))
return value
else:
article_list = self.get_queryset_data()
cache.set(cache_key, article_list)
logger.info('set view cache.key:{key}'.format(key=cache_key))
return article_list
def get_queryset(self):
"""
获取查询集 - 从缓存获取数据
Returns:
QuerySet: 文章查询集
"""
key = self.get_queryset_cache_key()
value = self.get_queryset_from_cache(key)
return value
def get_context_data(self, **kwargs):
"""添加上下文数据"""
kwargs['linktype'] = self.link_type
return super(ArticleListView, self).get_context_data(**kwargs)
class IndexView(ArticleListView):
"""
首页视图
显示最新的文章列表
"""
# 友情链接类型 - 首页显示
link_type = LinkShowType.I
def get_queryset_data(self):
"""获取首页文章数据 - 只获取已发布的普通文章"""
article_list = Article.objects.filter(type='a', status='p')
return article_list
def get_queryset_cache_key(self):
"""获取首页缓存键 - 基于页码"""
cache_key = 'index_{page}'.format(page=self.page_number)
return cache_key
class ArticleDetailView(DetailView):
"""
文章详情页面视图
显示单篇文章的详细内容和评论
"""
template_name = 'blog/article_detail.html'
model = Article # 关联的模型
pk_url_kwarg = 'article_id' # URL中主键参数的名称
context_object_name = "article" # 模板中使用的变量名
def get_context_data(self, **kwargs):
"""添加上下文数据 - 文章详情和评论信息"""
# 创建评论表单
comment_form = CommentForm()
# 获取文章评论列表
article_comments = self.object.comment_list()
# 获取顶级评论(没有父评论的评论)
parent_comments = article_comments.filter(parent_comment=None)
# 获取博客设置
blog_setting = get_blog_setting()
# 对评论进行分页
paginator = Paginator(parent_comments, blog_setting.article_comment_count)
page = self.request.GET.get('comment_page', '1')
# 验证页码
if not page.isnumeric():
page = 1
else:
page = int(page)
if page < 1:
page = 1
if page > paginator.num_pages:
page = paginator.num_pages
# 获取当前页的评论
p_comments = paginator.page(page)
# 计算下一页和上一页
next_page = p_comments.next_page_number() if p_comments.has_next() else None
prev_page = p_comments.previous_page_number() if p_comments.has_previous() else None
# 构建评论分页URL
if next_page:
kwargs[
'comment_next_page_url'] = self.object.get_absolute_url() + f'?comment_page={next_page}#commentlist-container'
if prev_page:
kwargs[
'comment_prev_page_url'] = self.object.get_absolute_url() + f'?comment_page={prev_page}#commentlist-container'
# 添加上下文数据
kwargs['form'] = comment_form
kwargs['article_comments'] = article_comments
kwargs['p_comments'] = p_comments
kwargs['comment_count'] = len(article_comments) if article_comments else 0
# 添加上下篇文章信息
kwargs['next_article'] = self.object.next_article
kwargs['prev_article'] = self.object.prev_article
# 调用父类方法获取基础上下文
context = super(ArticleDetailView, self).get_context_data(**kwargs)
article = self.object
# Action Hook, 通知插件"文章详情已获取"
hooks.run_action('after_article_body_get', article=article, request=self.request)
# Filter Hook, 允许插件修改文章正文
article.body = hooks.apply_filters(ARTICLE_CONTENT_HOOK_NAME, article.body, article=article,
request=self.request)
return context
class CategoryDetailView(ArticleListView):
"""
分类目录列表视图
显示指定分类下的所有文章包括子分类
"""
page_type = "分类目录归档"
def get_queryset_data(self):
"""获取分类文章数据 - 包括所有子分类的文章"""
slug = self.kwargs['category_name']
category = get_object_or_404(Category, slug=slug)
categoryname = category.name
self.categoryname = categoryname
# 获取所有子分类的名称
categorynames = list(
map(lambda c: c.name, category.get_sub_categorys()))
# 获取这些分类下的所有已发布文章
article_list = Article.objects.filter(
category__name__in=categorynames, status='p')
return article_list
def get_queryset_cache_key(self):
"""获取分类页面缓存键 - 基于分类名称和页码"""
slug = self.kwargs['category_name']
category = get_object_or_404(Category, slug=slug)
categoryname = category.name
self.categoryname = categoryname
cache_key = 'category_list_{categoryname}_{page}'.format(
categoryname=categoryname, page=self.page_number)
return cache_key
def get_context_data(self, **kwargs):
"""添加上下文数据"""
categoryname = self.categoryname
try:
categoryname = categoryname.split('/')[-1] # 处理多层分类名称
except BaseException:
pass
kwargs['page_type'] = CategoryDetailView.page_type
kwargs['tag_name'] = categoryname
return super(CategoryDetailView, self).get_context_data(**kwargs)
class AuthorDetailView(ArticleListView):
"""
作者详情页视图
显示指定作者的所有文章
"""
page_type = '作者文章归档'
def get_queryset_cache_key(self):
"""获取作者页面缓存键 - 基于作者名称和页码"""
from uuslug import slugify
author_name = slugify(self.kwargs['author_name']) # 使用slugify处理作者名
cache_key = 'author_{author_name}_{page}'.format(
author_name=author_name, page=self.page_number)
return cache_key
def get_queryset_data(self):
"""获取作者文章数据 - 指定作者的所有已发布文章"""
author_name = self.kwargs['author_name']
article_list = Article.objects.filter(
author__username=author_name, type='a', status='p')
return article_list
def get_context_data(self, **kwargs):
"""添加上下文数据"""
author_name = self.kwargs['author_name']
kwargs['page_type'] = AuthorDetailView.page_type
kwargs['tag_name'] = author_name
return super(AuthorDetailView, self).get_context_data(**kwargs)
class TagDetailView(ArticleListView):
"""
标签列表页面视图
显示指定标签下的所有文章
"""
page_type = '分类标签归档'
def get_queryset_data(self):
"""获取标签文章数据 - 指定标签的所有已发布文章"""
slug = self.kwargs['tag_name']
tag = get_object_or_404(Tag, slug=slug)
tag_name = tag.name
self.name = tag_name
article_list = Article.objects.filter(
tags__name=tag_name, type='a', status='p')
return article_list
def get_queryset_cache_key(self):
"""获取标签页面缓存键 - 基于标签名称和页码"""
slug = self.kwargs['tag_name']
tag = get_object_or_404(Tag, slug=slug)
tag_name = tag.name
self.name = tag_name
cache_key = 'tag_{tag_name}_{page}'.format(
tag_name=tag_name, page=self.page_number)
return cache_key
def get_context_data(self, **kwargs):
"""添加上下文数据"""
tag_name = self.name
kwargs['page_type'] = TagDetailView.page_type
kwargs['tag_name'] = tag_name
return super(TagDetailView, self).get_context_data(**kwargs)
class ArchivesView(ArticleListView):
"""
文章归档页面视图
按时间顺序显示所有文章不分页
"""
page_type = '文章归档'
paginate_by = None # 不分页
page_kwarg = None
template_name = 'blog/article_archives.html' # 使用专门的归档模板
def get_queryset_data(self):
"""获取归档数据 - 所有已发布文章"""
return Article.objects.filter(status='p').all()
def get_queryset_cache_key(self):
"""获取归档页面缓存键 - 固定键名"""
cache_key = 'archives'
return cache_key
class LinkListView(ListView):
"""
友情链接列表视图
显示所有启用的友情链接
"""
model = Links # 关联的模型
template_name = 'blog/links_list.html' # 友情链接模板
def get_queryset(self):
"""获取启用的友情链接"""
return Links.objects.filter(is_enable=True)
class EsSearchView(SearchView):
"""
Elasticsearch搜索视图
扩展Haystack的搜索功能
"""
def get_context(self):
"""获取搜索上下文数据"""
paginator, page = self.build_page() # 构建分页
context = {
"query": self.query, # 搜索关键词
"form": self.form, # 搜索表单
"page": page, # 当前页
"paginator": paginator, # 分页器
"suggestion": None, # 搜索建议
}
# 添加拼写建议
if hasattr(self.results, "query") and self.results.query.backend.include_spelling:
context["suggestion"] = self.results.query.get_spelling_suggestion()
context.update(self.extra_context())
return context
@csrf_exempt # 免除CSRF验证用于文件上传
def fileupload(request):
"""
文件上传视图
提供图床功能支持图片和文件上传
Args:
request: HTTP请求对象
Returns:
HttpResponse: 上传结果
"""
if request.method == 'POST':
# 验证签名,确保上传请求合法
sign = request.GET.get('sign', None)
if not sign:
return HttpResponseForbidden()
if not sign == get_sha256(get_sha256(settings.SECRET_KEY)):
return HttpResponseForbidden()
response = []
# 处理所有上传的文件
for filename in request.FILES:
# 按日期创建目录结构
timestr = timezone.now().strftime('%Y/%m/%d')
imgextensions = ['jpg', 'png', 'jpeg', 'bmp'] # 图片文件扩展名
fname = u''.join(str(filename))
# 判断是否为图片文件
isimage = len([i for i in imgextensions if fname.find(i) >= 0]) > 0
# 创建存储目录
base_dir = os.path.join(settings.STATICFILES, "files" if not isimage else "image", timestr)
if not os.path.exists(base_dir):
os.makedirs(base_dir)
# 生成唯一文件名
savepath = os.path.normpath(os.path.join(base_dir, f"{uuid.uuid4().hex}{os.path.splitext(filename)[-1]}"))
# 安全检查:确保文件保存在指定目录内
if not savepath.startswith(base_dir):
return HttpResponse("only for post")
# 保存文件
with open(savepath, 'wb+') as wfile:
for chunk in request.FILES[filename].chunks():
wfile.write(chunk)
# 如果是图片,进行压缩优化
if isimage:
from PIL import Image
image = Image.open(savepath)
image.save(savepath, quality=20, optimize=True) # 压缩质量20%
# 生成静态文件URL
url = static(savepath)
response.append(url)
return HttpResponse(response)
else:
return HttpResponse("only for post") # 只支持POST请求
def page_not_found_view(
request,
exception,
template_name='blog/error_page.html'):
"""
404页面未找到视图
Args:
request: 请求对象
exception: 异常信息
template_name: 模板名称
Returns:
HttpResponse: 404错误页面
"""
if exception:
logger.error(exception) # 记录异常日志
url = request.get_full_path()
return render(request,
template_name,
{'message': _('Sorry, the page you requested is not found, please click the home page to see other?'),
'statuscode': '404'},
status=404)
def server_error_view(request, template_name='blog/error_page.html'):
"""
500服务器错误视图
Args:
request: 请求对象
template_name: 模板名称
Returns:
HttpResponse: 500错误页面
"""
return render(request,
template_name,
{'message': _('Sorry, the server is busy, please click the home page to see other?'),
'statuscode': '500'},
status=500)
def permission_denied_view(
request,
exception,
template_name='blog/error_page.html'):
"""
403权限拒绝视图
Args:
request: 请求对象
exception: 异常信息
template_name: 模板名称
Returns:
HttpResponse: 403错误页面
"""
if exception:
logger.error(exception) # 记录异常日志
return render(
request, template_name, {
'message': _('Sorry, you do not have permission to access this page?'),
'statuscode': '403'}, status=403)
def clean_cache_view(request):
"""
清理缓存视图
用于手动清理系统缓存
Args:
request: 请求对象
Returns:
HttpResponse: 清理结果
"""
cache.clear()
return HttpResponse('ok')
>>>>>>> hyt_branch

File diff suppressed because it is too large Load Diff

@ -0,0 +1,28 @@
"""
WSGI config for djangoblog project.
Django博客项目的WSGI配置文件
WSGIWeb Server Gateway Interface是Web服务器与Python Web应用之间的通信标准
负责将Web服务器如NginxApache接收的HTTP请求转发给Django应用再将应用响应返回给服务器
It exposes the WSGI callable as a module-level variable named ``application``.
该文件将WSGI可调用对象处理请求的核心入口暴露为模块级变量命名为`application`
Web服务器通过调用这个`application`对象与Django应用交互
For more information on this file, see
https://docs.djangoproject.com/en/1.10/howto/deployment/wsgi/
"""
# 导入Python内置的os模块用于读取环境变量、处理路径等
import os
# 导入Django的WSGI应用生成器根据项目配置创建WSGI可调用对象
from django.core.wsgi import get_wsgi_application
# 设置Django项目的配置模块环境变量
# 告诉Django使用哪个settings文件此处为项目根目录下的djangoblog.settings
# 生产环境中可通过服务器配置修改该环境变量,切换不同配置(如生产/测试配置)
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "djangoblog.settings")
# 创建WSGI应用对象Django根据上述配置生成处理HTTP请求的核心入口
# Web服务器如Gunicorn、uWSGI会加载这个`application`对象来运行项目
application = get_wsgi_application()
Loading…
Cancel
Save