代码注释 #8

Open
plqo32bax wants to merge 85 commits from hjn_branch into develop

BIN
.gitignore vendored

Binary file not shown.

@ -1,15 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module version="4">
<component name="PyDocumentationSettings">
<option name="format" value="PLAIN" />
<option name="myDocStringFormat" value="Plain" />
</component>
<component name="TemplatesService">
<option name="TEMPLATE_CONFIGURATION" value="Jinja2" />
<option name="TEMPLATE_FOLDERS">
<list>
<option value="$MODULE_DIR$/src/DjangoBlog-master/DjangoBlog-master/templates" />
</list>
</option>
</component>
</module>

@ -1,68 +0,0 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="PyPackageRequirementsInspection" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignoredPackages">
<list>
<option value="bleach" />
<option value="borax" />
<option value="bottle" />
<option value="cffi" />
<option value="charset-normalizer" />
<option value="colorama" />
<option value="coverage" />
<option value="django-appconf" />
<option value="django-compressor" />
<option value="django-echarts" />
<option value="django-ipware" />
<option value="django-mdeditor" />
<option value="django-uuslug" />
<option value="elasticsearch-dsl" />
<option value="frozenlist" />
<option value="gevent" />
<option value="greenlet" />
<option value="htmlgenerator" />
<option value="idna" />
<option value="jieba" />
<option value="Jinja2" />
<option value="jsonpickle" />
<option value="MarkupSafe" />
<option value="multidict" />
<option value="mysqlclient" />
<option value="openai" />
<option value="prettytable" />
<option value="propcache" />
<option value="pycparser" />
<option value="pyecharts" />
<option value="Pygments" />
<option value="python-dateutil" />
<option value="python-ipware" />
<option value="python-logstash" />
<option value="python-slugify" />
<option value="pytz" />
<option value="rcssmin" />
<option value="redis" />
<option value="requests" />
<option value="rjsmin" />
<option value="setuptools" />
<option value="simplejson" />
<option value="six" />
<option value="text-unidecode" />
<option value="tqdm" />
<option value="typing_extensions" />
<option value="ua-parser" />
<option value="ua-parser-builtins" />
<option value="user-agents" />
<option value="wcwidth" />
<option value="webencodings" />
<option value="WeRoBot" />
<option value="Whoosh" />
<option value="xmltodict" />
<option value="yarl" />
<option value="zope.event" />
<option value="zope.interface" />
</list>
</option>
</inspection_tool>
</profile>
</component>

@ -1,6 +0,0 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Black">
<option name="sdkName" value="Python 3.12" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.12" project-jdk-type="Python SDK" />
</project>

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

@ -4,11 +4,7 @@
<option name="autoReloadType" value="SELECTIVE" />
</component>
<component name="ChangeListManager">
<list default="true" id="feebdecb-aab4-468a-89d2-02ab184d6524" name="更改" comment="">
<change beforePath="$PROJECT_DIR$/.idea/django.iml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/django.iml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/DjangoBlog-master/.idea/inspectionProfiles/profiles_settings.xml" beforeDir="false" afterPath="$PROJECT_DIR$/src/DjangoBlog-master/.idea/inspectionProfiles/profiles_settings.xml" afterDir="false" />
</list>
<list default="true" id="feebdecb-aab4-468a-89d2-02ab184d6524" name="更改" comment="注释" />
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
@ -17,7 +13,7 @@
<component name="Git.Settings">
<option name="RECENT_BRANCH_BY_REPOSITORY">
<map>
<entry key="$PROJECT_DIR$" value="main" />
<entry key="$PROJECT_DIR$" value="gjw_branch" />
</map>
</option>
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
@ -31,23 +27,23 @@
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent">{
&quot;keyToString&quot;: {
&quot;ModuleVcsDetector.initialDetectionPerformed&quot;: &quot;true&quot;,
&quot;RunOnceActivity.OpenDjangoStructureViewOnStart&quot;: &quot;true&quot;,
&quot;RunOnceActivity.ShowReadmeOnStart&quot;: &quot;true&quot;,
&quot;RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252&quot;: &quot;true&quot;,
&quot;RunOnceActivity.git.unshallow&quot;: &quot;true&quot;,
&quot;git-widget-placeholder&quot;: &quot;gjw__branch&quot;,
&quot;last_opened_file_path&quot;: &quot;C:/Users/35500/Desktop/软件工程与方法学/django&quot;,
&quot;node.js.detected.package.eslint&quot;: &quot;true&quot;,
&quot;node.js.detected.package.tslint&quot;: &quot;true&quot;,
&quot;node.js.selected.package.eslint&quot;: &quot;(autodetect)&quot;,
&quot;node.js.selected.package.tslint&quot;: &quot;(autodetect)&quot;,
&quot;nodejs_package_manager_path&quot;: &quot;npm&quot;,
&quot;vue.rearranger.settings.migration&quot;: &quot;true&quot;
<component name="PropertiesComponent"><![CDATA[{
"keyToString": {
"ModuleVcsDetector.initialDetectionPerformed": "true",
"RunOnceActivity.OpenDjangoStructureViewOnStart": "true",
"RunOnceActivity.ShowReadmeOnStart": "true",
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
"RunOnceActivity.git.unshallow": "true",
"git-widget-placeholder": "master",
"last_opened_file_path": "C:/Users/35500/Desktop/软件工程与方法学/django",
"node.js.detected.package.eslint": "true",
"node.js.detected.package.tslint": "true",
"node.js.selected.package.eslint": "(autodetect)",
"node.js.selected.package.tslint": "(autodetect)",
"nodejs_package_manager_path": "npm",
"vue.rearranger.settings.migration": "true"
}
}</component>
}]]></component>
<component name="RunManager">
<configuration name="django" type="Python.DjangoServer" factoryName="Django server">
<module name="django" />

@ -1,2 +0,0 @@
# Django

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

@ -1,3 +0,0 @@
# 默认忽略的文件
/shelf/
/workspace.xml

@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?> <!-- XML文件声明指定版本为1.0编码为UTF-8 -->
<module type="PYTHON_MODULE" version="4"> <!-- 定义模块类型为Python模块版本4 -->
<component name="NewModuleRootManager"> <!-- 新模块根管理器组件,用于管理模块的根路径等 -->
<content url="file://$MODULE_DIR$" /> <!-- 配置模块内容的路径,指向当前模块所在目录 -->
<orderEntry type="jdk" jdkName="Python 3.12" jdkType="Python SDK" /> <!-- 配置Python SDK指定使用Python 3.12版本 -->
<orderEntry type="sourceFolder" forTests="false" /> <!-- 配置源码文件夹,且该文件夹不用于测试 -->
</component>
<component name="PyDocumentationSettings"> <!-- Python文档设置组件控制文档相关格式 -->
<option name="format" value="PLAIN" /> <!-- 设置文档格式为纯文本PLAIN -->
<option name="myDocStringFormat" value="Plain" /> <!-- 设置自定义文档字符串格式为纯文本Plain -->
</component>
</module> <!-- 模块定义结束 -->

@ -1,6 +0,0 @@
<component name="InspectionProjectProfileManager">
<settings><!-- 配置设置区域 -->
<option name="USE_PROJECT_PROFILE" value="false" /><!-- 配置选项是否使用项目配置文件当前设置为不使用false -->
<version value="1.0" /><!-- 配置版本信息当前版本为1.0 -->
</settings>
</component>

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Black">
<option name="sdkName" value="Python 3.12 (pythonProject)" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.12" project-jdk-type="Python SDK" />
</project>

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/DjangoBlog-master.iml" filepath="$PROJECT_DIR$/.idea/DjangoBlog-master.iml" />
</modules>
</component>
</project>

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4"> #11
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/DjangoBlog" vcs="Git" />
</component>
</project>

@ -1 +0,0 @@
Subproject commit 76918f2c7f4bd4db4ad3877be1ee20c256446d21

@ -1,5 +0,0 @@
from django.apps import AppConfig
class AccountsConfig(AppConfig):
name = 'accounts'

@ -1,28 +0,0 @@
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'),
]

@ -1,47 +0,0 @@
from django.contrib import admin
from django.urls import reverse
from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _
def disable_commentstatus(modeladmin, request, queryset):
queryset.update(is_enable=False)
def enable_commentstatus(modeladmin, request, queryset):
queryset.update(is_enable=True)
disable_commentstatus.short_description = _('Disable comments')
enable_commentstatus.short_description = _('Enable comments')
class CommentAdmin(admin.ModelAdmin):
list_per_page = 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):
info = (obj.author._meta.app_label, obj.author._meta.model_name)
link = reverse('admin:%s_%s_change' % info, args=(obj.author.id,))
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):
info = (obj.article._meta.app_label, obj.article._meta.model_name)
link = reverse('admin:%s_%s_change' % info, args=(obj.article.id,))
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')

@ -1,13 +0,0 @@
from django import forms
from django.forms import ModelForm
from .models import Comment
class CommentForm(ModelForm):
parent_comment_id = forms.IntegerField(
widget=forms.HiddenInput, required=False)
class Meta:
model = Comment
fields = ['body']

@ -1,18 +0,0 @@
# Generated by Django 4.1.7 on 2023-04-24 13:48
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('comments', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='comment',
name='is_enable',
field=models.BooleanField(default=False, verbose_name='是否显示'),
),
]

@ -1,39 +0,0 @@
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
# Create your models here.
class Comment(models.Model):
body = models.TextField('正文', max_length=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,
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']
verbose_name = _('comment')
verbose_name_plural = verbose_name
get_latest_by = 'id'
def __str__(self):
return self.body

@ -1,11 +0,0 @@
from django.urls import path
from . import views
app_name = "comments"
urlpatterns = [
path(
'article/<int:article_id>/postcomment',
views.CommentPostView.as_view(),
name='postcomment'),
]

@ -1,38 +0,0 @@
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')
article_url = f"https://{site}{comment.article.get_absolute_url()}"
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([tomail], subject, html_content)
try:
if comment.parent_comment:
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)

@ -1,63 +0,0 @@
# Create your views here.
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
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
class CommentPostView(FormView):
form_class = CommentForm
template_name = 'blog/article_detail.html'
@method_decorator(csrf_protect)
def dispatch(self, *args, **kwargs):
return super(CommentPostView, self).dispatch(*args, **kwargs)
def get(self, request, *args, **kwargs):
article_id = self.kwargs['article_id']
article = get_object_or_404(Article, pk=article_id)
url = article.get_absolute_url()
return HttpResponseRedirect(url + "#comments")
def form_invalid(self, form):
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
author = BlogUser.objects.get(pk=user.pk)
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
if form.cleaned_data['parent_comment_id']:
parent_comment = Comment.objects.get(
pk=form.cleaned_data['parent_comment_id'])
comment.parent_comment = parent_comment
comment.save(True)
return HttpResponseRedirect(
"%s#div-comment-%d" %
(article.get_absolute_url(), comment.pk))

@ -1 +1,4 @@
default_app_config = 'djangoblog.apps.DjangoblogAppConfig'
# 默认应用配置指定当前Django应用djangoblog对应的配置类
# 配置类位于 djangoblog/apps.py 文件中的 DjangoBlogAppConfig 类

@ -1,64 +1,90 @@
# 导入Django内置的AdminSite后台管理站点基类
from django.contrib.admin import AdminSite
# 导入日志条目模型(记录后台操作日志)
from django.contrib.admin.models import LogEntry
# 导入站点管理类和站点模型Django内置的多站点配置
from django.contrib.sites.admin import SiteAdmin
from django.contrib.sites.models import Site
from accounts.admin import *
from blog.admin import *
from blog.models import *
from comments.admin import *
from comments.models import *
from djangoblog.logentryadmin import LogEntryAdmin
from oauth.admin import *
from oauth.models import *
from owntracks.admin import *
from owntracks.models import *
from servermanager.admin import *
from servermanager.models import *
# 导入各个应用的Admin类和模型项目内自定义的后台配置
from accounts.admin import * # 账号相关的后台管理配置
from blog.admin import * # 博客核心功能的后台管理配置
from blog.models import * # 博客核心模型(文章、分类等)
from comments.admin import * # 评论功能的后台管理配置
from comments.models import *# 评论模型
from djangoblog.logentryadmin import LogEntryAdmin # 自定义的日志条目管理类
from oauth.admin import * # OAuth第三方登录的后台管理配置
from oauth.models import * # OAuth相关模型
from owntracks.admin import *# OwnTracks位置追踪的后台管理配置
from owntracks.models import *# OwnTracks相关模型
from servermanager.admin import *# 服务器管理的后台管理配置
from servermanager.models import *# 服务器管理相关模型
# 自定义Django后台管理站点类继承自AdminSite
class DjangoBlogAdminSite(AdminSite):
# 后台页面顶部的标题
site_header = 'djangoblog administration'
# 浏览器标签页的标题
site_title = 'djangoblog site admin'
# 初始化方法
def __init__(self, name='admin'):
# 调用父类AdminSite的初始化方法
super().__init__(name)
# 权限验证:判断用户是否有权限访问后台
def has_permission(self, request):
# 只有超级用户才能访问后台
return request.user.is_superuser
# 注释掉的代码自定义后台URL路由
# def get_urls(self):
# # 先获取父类默认的URL
# urls = super().get_urls()
# # 导入path用于定义路由
# from django.urls import path
# # 导入自定义的视图函数(刷新缓存)
# from blog.views import refresh_memcache
#
# # 自定义路由:后台添加“刷新缓存”的功能入口
# my_urls = [
# path('refresh/', self.admin_view(refresh_memcache), name="refresh"),
# ]
# # 合并默认路由和自定义路由
# return urls + my_urls
# 实例化自定义的后台管理站点
admin_site = DjangoBlogAdminSite(name='admin')
admin_site.register(Article, ArticlelAdmin)
admin_site.register(Category, CategoryAdmin)
admin_site.register(Tag, TagAdmin)
admin_site.register(Links, LinksAdmin)
admin_site.register(SideBar, SideBarAdmin)
admin_site.register(BlogSettings, BlogSettingsAdmin)
# 注册博客核心模型到后台管理并指定对应的Admin类配置模型在后台的展示、操作
admin_site.register(Article, ArticlelAdmin) # 文章模型
admin_site.register(Category, CategoryAdmin)# 分类模型
admin_site.register(Tag, TagAdmin) # 标签模型
admin_site.register(Links, LinksAdmin) # 友情链接模型
admin_site.register(SideBar, SideBarAdmin) # 侧边栏模型
admin_site.register(BlogSettings, BlogSettingsAdmin)# 博客设置模型
admin_site.register(commands, CommandsAdmin)
admin_site.register(EmailSendLog, EmailSendLogAdmin)
# 注册服务器管理相关模型
admin_site.register(commands, CommandsAdmin)# 命令模型
admin_site.register(EmailSendLog, EmailSendLogAdmin)# 邮件发送日志模型
admin_site.register(BlogUser, BlogUserAdmin)
# 注册账号相关模型
admin_site.register(BlogUser, BlogUserAdmin)# 博客用户模型
admin_site.register(Comment, CommentAdmin)
# 注册评论相关模型
admin_site.register(Comment, CommentAdmin)# 评论模型
admin_site.register(OAuthUser, OAuthUserAdmin)
admin_site.register(OAuthConfig, OAuthConfigAdmin)
# 注册OAuth第三方登录相关模型
admin_site.register(OAuthUser, OAuthUserAdmin)# OAuth用户模型
admin_site.register(OAuthConfig, OAuthConfigAdmin)# OAuth配置模型
admin_site.register(OwnTrackLog, OwnTrackLogsAdmin)
# 注册OwnTracks位置追踪相关模型
admin_site.register(OwnTrackLog, OwnTrackLogsAdmin)# 位置追踪日志模型
# 注册Django内置的站点模型多站点配置
admin_site.register(Site, SiteAdmin)
admin_site.register(LogEntry, LogEntryAdmin)
# 注册日志条目模型后台操作日志使用自定义的LogEntryAdmin
admin_site.register(LogEntry, LogEntryAdmin)

@ -1,11 +1,20 @@
# 导入Django的应用配置基类AppConfig
from django.apps import AppConfig
# 定义当前应用djangoblog的配置类继承自AppConfig
class DjangoblogAppConfig(AppConfig):
# 指定模型主键的默认类型为BigAutoField大整数自增主键
# 替代旧版默认的AutoField支持更大的数值范围
default_auto_field = 'django.db.models.BigAutoField'
# 当前应用的名称(必须与项目中应用的目录名一致)
name = 'djangoblog'
# 应用启动时自动执行的方法Django加载完应用后触发
def ready(self):
# 先调用父类的ready方法确保基础初始化完成
super().ready()
# Import and load plugins here
# 在这里导入并加载插件(应用启动时自动加载插件逻辑)
# 从当前应用的plugin_manage模块中导入load_plugins函数
from .plugin_manage.loader import load_plugins
load_plugins()
# 执行插件加载操作
load_plugins()

@ -1,69 +1,91 @@
# 导入线程模块(用于异步执行任务)
import _thread
# 导入日志模块(记录运行信息/错误)
import logging
# 导入Django信号相关工具
import django.dispatch
from django.conf import settings
from django.contrib.admin.models import LogEntry
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
# 导入模型保存后的内置信号
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.dispatch import receiver # 信号接收器装饰器
from comments.models import Comment
from comments.utils import send_comment_email
from djangoblog.spider_notify import SpiderNotify
# 导入自定义模型和工具函数
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
from djangoblog.utils import get_current_site # 获取当前站点域名
from oauth.models import OAuthUser # OAuth第三方用户模型
# 创建当前模块的日志对象
logger = logging.getLogger(__name__)
# 自定义信号1OAuth用户登录信号携带参数id
oauth_user_login_signal = django.dispatch.Signal(['id'])
send_email_signal = django.dispatch.Signal(
['emailto', 'title', 'content'])
# 自定义信号2发送邮件信号携带参数收件人、标题、内容
send_email_signal = django.dispatch.Signal(['emailto', 'title', 'content'])
# 监听send_email_signal信号的处理器
@receiver(send_email_signal)
def send_email_signal_handler(sender, **kwargs):
emailto = kwargs['emailto']
title = kwargs['title']
content = kwargs['content']
# 从信号参数中提取邮件信息
emailto = kwargs['emailto'] # 收件人列表
title = kwargs['title'] # 邮件标题
content = kwargs['content'] # 邮件内容HTML格式
# 构造多格式邮件对象
msg = EmailMultiAlternatives(
title,
content,
from_email=settings.DEFAULT_FROM_EMAIL,
to=emailto)
msg.content_subtype = "html"
title, # 邮件标题
content, # 邮件内容(文本/HTML
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)
log.emailto = ','.join(emailto) # 把收件人列表转成字符串存储
try:
# 发送邮件,返回成功发送的数量
result = msg.send()
log.send_result = result > 0
log.send_result = result > 0 # 发送成功则标记为True
except Exception as e:
# 发送失败时记录错误日志
logger.error(f"失败邮箱号: {emailto}, {e}")
log.send_result = False
log.save()
log.save() # 保存日志记录
# 监听oauth_user_login_signal信号的处理器
@receiver(oauth_user_login_signal)
def oauth_user_login_signal_handler(sender, **kwargs):
id = kwargs['id']
oauthuser = OAuthUser.objects.get(id=id)
site = get_current_site().domain
id = kwargs['id'] # 从信号参数中提取OAuth用户ID
oauthuser = OAuthUser.objects.get(id=id) # 获取对应的OAuth用户对象
site = get_current_site().domain # 获取当前站点的域名
# 若用户头像链接不是本站域名,则下载并保存到本地
if oauthuser.picture and not oauthuser.picture.find(site) >= 0:
from djangoblog.utils import save_user_avatar
oauthuser.picture = save_user_avatar(oauthuser.picture)
oauthuser.save()
oauthuser.picture = save_user_avatar(oauthuser.picture) # 下载并替换头像链接
oauthuser.save() # 保存修改
delete_sidebar_cache()
delete_sidebar_cache() # 清除侧边栏缓存(避免显示旧数据)
# 监听所有模型post_save信号的处理器模型保存后触发
@receiver(post_save)
def model_post_save_callback(
sender,
@ -73,50 +95,71 @@ def model_post_save_callback(
using,
update_fields,
**kwargs):
clearcache = False
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()
SpiderNotify.baidu_notify([notify_url])
notify_url = instance.get_full_url() # 获取内容的完整URL
SpiderNotify.baidu_notify([notify_url]) # 通知百度爬虫
except Exception as ex:
logger.error("notify sipder", ex)
logger.error("notify sipder", ex) # 通知失败记录错误
# 不是仅更新阅读量 → 需要清除缓存
if not is_update_views:
clearcache = True
# 若保存的是Comment评论实例
if isinstance(instance, Comment):
# 仅当评论是启用状态时处理
if instance.is_enable:
# 获取评论对应的文章URL
path = instance.article.get_absolute_url()
site = get_current_site().domain
site = get_current_site().domain # 获取当前站点域名
# 处理带端口的域名如localhost:8000 → 取localhost
if site.find(':') > 0:
site = site[0:site.find(':')]
# 过期文章详情页的缓存(确保显示最新评论)
expire_view_cache(
path,
servername=site,
serverport=80,
key_prefix='blogdetail')
key_prefix='blogdetail'
)
# 清除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_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):
# 用户存在且有用户名时处理
if user and user.username:
logger.info(user)
delete_sidebar_cache()
# cache.clear()
logger.info(user) # 记录用户登录/登出日志
delete_sidebar_cache() # 清除侧边栏缓存(避免显示旧的用户相关内容)
# cache.clear() # (注释)可选择清空整个缓存,此处未启用

@ -1,183 +1,144 @@
# 导入Django字符串处理工具及Elasticsearch相关依赖
from django.utils.encoding import force_str
from elasticsearch_dsl import Q
# 导入Haystack搜索引擎基础类和工具
from haystack.backends import BaseEngine, BaseSearchBackend, BaseSearchQuery, log_query
from haystack.forms import ModelSearchForm
from haystack.models import SearchResult
from haystack.utils import log as logging
# 导入博客相关的ES文档类、管理类和模型
from blog.documents import ArticleDocument, ArticleDocumentManager
from blog.models import Article
# 创建日志对象
logger = logging.getLogger(__name__)
# 自定义Elasticsearch搜索后端继承Haystack基础搜索后端
class ElasticSearchBackend(BaseSearchBackend):
def __init__(self, connection_alias, **connection_options):
super(
ElasticSearchBackend,
self).__init__(
connection_alias,
**connection_options)
self.manager = ArticleDocumentManager()
self.include_spelling = True
super().__init__(connection_alias,** connection_options)
self.manager = ArticleDocumentManager() # 初始化文档管理器
self.include_spelling = True # 启用拼写建议功能
# 转换模型实例为ES文档格式
def _get_models(self, iterable):
models = iterable if iterable and iterable[0] else Article.objects.all()
docs = self.manager.convert_to_doc(models)
return docs
models = iterable if (iterable and iterable[0]) else Article.objects.all()
return self.manager.convert_to_doc(models)
# 创建索引并批量导入文档
def _create(self, models):
self.manager.create_index()
docs = self._get_models(models)
self.manager.rebuild(docs)
self.manager.rebuild(self._get_models(models))
# 删除指定模型对应的ES文档
def _delete(self, models):
for m in models:
m.delete()
return True
# 重建索引,更新文档数据
def _rebuild(self, models):
models = models if models else Article.objects.all()
docs = self.manager.convert_to_doc(models)
self.manager.update_docs(docs)
self.manager.update_docs(self.manager.convert_to_doc(models))
# 批量更新ES文档
def update(self, index, iterable, commit=True):
self.manager.update_docs(self._get_models(iterable))
models = self._get_models(iterable)
self.manager.update_docs(models)
# 移除单个对象对应的ES文档
def remove(self, obj_or_string):
models = self._get_models([obj_or_string])
self._delete(models)
self._delete(self._get_models([obj_or_string]))
# 清空索引数据
def clear(self, models=None, commit=True):
self.remove(None)
@staticmethod
# 获取搜索推荐词,无推荐则返回原搜索词
def get_suggestion(query: str) -> str:
"""获取推荐词, 如果没有找到添加原搜索词"""
search = ArticleDocument.search() \
.query("match", body=query) \
.suggest('suggest_search', query, term={'field': 'body'}) \
.execute()
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"])
keywords.append(suggest["options"][0]["text"] if suggest["options"] else suggest["text"])
return ' '.join(keywords)
# 核心搜索方法,带日志记录
@log_query
def search(self, query_string, **kwargs):
logger.info('search query_string:' + query_string)
start_offset, end_offset = kwargs.get('start_offset'), kwargs.get('end_offset')
start_offset = kwargs.get('start_offset')
end_offset = kwargs.get('end_offset')
# 推荐词搜索
if getattr(self, "is_suggest", None):
suggestion = self.get_suggestion(query_string)
else:
suggestion = query_string
q = Q('bool',
should=[Q('match', body=suggestion), Q('match', title=suggestion)],
minimum_should_match="70%")
search = ArticleDocument.search() \
.query('bool', filter=[q]) \
.filter('term', status='p') \
.filter('term', type='a') \
.source(False)[start_offset: end_offset]
# 处理搜索推荐词
suggestion = self.get_suggestion(query_string) if getattr(self, "is_suggest", None) else query_string
# 构建搜索条件匹配正文和标题最低匹配度70%
q = Q('bool', should=[Q('match', body=suggestion), Q('match', title=suggestion)], minimum_should_match="70%")
# 执行搜索:筛选已发布文章,指定结果范围
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 = {}
result_class = SearchResult
result = result_class(
app_label,
model_name,
raw_result['_id'],
raw_result['_score'],
**additional_fields)
result = SearchResult('blog', 'Article', raw_result['_id'], raw_result['_score'])
raw_results.append(result)
facets = {}
spelling_suggestion = None if query_string == suggestion else suggestion
return {
'results': raw_results,
'hits': hits,
'facets': facets,
'spelling_suggestion': spelling_suggestion,
}
# 返回搜索结果、总数、推荐词等
spelling_suggestion = None if query_string == suggestion else suggestion
return {'results': raw_results, 'hits': hits, 'facets': {}, 'spelling_suggestion': spelling_suggestion}
# 自定义搜索查询类继承Haystack基础查询类
class ElasticSearchQuery(BaseSearchQuery):
# 转换时间格式适配搜索
def _convert_datetime(self, date):
if hasattr(date, 'hour'):
return force_str(date.strftime('%Y%m%d%H%M%S'))
else:
return force_str(date.strftime('%Y%m%d000000'))
fmt = '%Y%m%d%H%M%S' if hasattr(date, 'hour') else '%Y%m%d000000'
return force_str(date.strftime(fmt))
# 清洗查询语句,处理保留词和字符
def clean(self, query_fragment):
"""
Provides a mechanism for sanitizing user input before presenting the
value to the backend.
Whoosh 1.X differs here in that you can no longer use a backslash
to escape reserved characters. Instead, the whole word should be
quoted.
"""
words = query_fragment.split()
cleaned_words = []
for word in words:
for word in query_fragment.split():
if word in self.backend.RESERVED_WORDS:
word = word.replace(word, word.lower())
word = word.lower()
for char in self.backend.RESERVED_CHARACTERS:
if char in word:
word = "'%s'" % word
word = f"'{word}'"
break
cleaned_words.append(word)
return ' '.join(cleaned_words)
# 构建查询片段
def build_query_fragment(self, field, filter_type, value):
return value.query_string
# 获取搜索结果总数
def get_count(self):
results = self.get_results()
return len(results) if results else 0
return len(self.get_results()) if self.get_results() else 0
# 获取拼写建议
def get_spelling_suggestion(self, preferred_query=None):
return self._spelling_suggestion
# 构建查询参数
def build_params(self, spelling_query=None):
kwargs = super(ElasticSearchQuery, self).build_params(spelling_query=spelling_query)
return kwargs
return super().build_params(spelling_query=spelling_query)
# 自定义搜索表单继承Haystack模型搜索表单
class ElasticSearchModelSearchForm(ModelSearchForm):
# 重写搜索方法,控制是否启用搜索建议
def search(self):
# 是否建议搜索
self.searchqueryset.query.backend.is_suggest = self.data.get("is_suggest") != "no"
sqs = super().search()
return sqs
return super().search()
# 自定义搜索引擎引擎类,指定后端和查询类
class ElasticSearchEngine(BaseEngine):
backend = ElasticSearchBackend
query = ElasticSearchQuery
query = ElasticSearchQuery

@ -1,40 +1,51 @@
# 导入用户模型、RSS订阅核心类等依赖
from django.contrib.auth import get_user_model
from django.contrib.syndication.views import Feed
from django.utils import timezone
from django.utils.feedgenerator import Rss201rev2Feed
from django.contrib.syndication.views import Feed # Django RSS订阅基础类
from django.utils import timezone # 时间处理工具
from django.utils.feedgenerator import Rss201rev2Feed # RSS2.0格式生成器
# 导入博客文章模型和Markdown转换工具
from blog.models import Article
from djangoblog.utils import CommonMarkdown
# 自定义博客RSS订阅类继承Django的Feed基类
class DjangoBlogFeed(Feed):
feed_type = Rss201rev2Feed
feed_type = Rss201rev2Feed # 指定订阅源格式为RSS2.0
description = '大巧无工,重剑无锋.'
title = "且听风吟 大巧无工,重剑无锋. "
link = "/feed/"
description = '大巧无工,重剑无锋.' # 订阅源描述
title = "且听风吟 大巧无工,重剑无锋. " # 订阅源标题
link = "/feed/" # 订阅源的链接地址
# 订阅源作者名称,取第一个用户的昵称
def author_name(self):
return get_user_model().objects.first().nickname
# 订阅源作者的链接,取第一个用户的个人页面地址
def author_link(self):
return get_user_model().objects.first().get_absolute_url()
# 订阅源的内容项获取5篇已发布的文章按发布时间倒序
def items(self):
return Article.objects.filter(type='a', status='p').order_by('-pub_time')[:5]
# 单个内容项的标题(对应文章标题)
def item_title(self, item):
return item.title
# 单个内容项的描述将文章正文Markdown格式转为HTML
def item_description(self, item):
return CommonMarkdown.get_markdown(item.body)
# 订阅源的版权信息,动态显示当前年份
def feed_copyright(self):
now = timezone.now()
return "Copyright© {year} 且听风吟".format(year=now.year)
# 单个内容项的链接(对应文章详情页地址)
def item_link(self, item):
return item.get_absolute_url()
# 单个内容项的唯一标识此处未实现可补充文章ID等作为标识
def item_guid(self, item):
return
return

@ -1,91 +1,77 @@
# 导入Django后台管理及相关工具类
from django.contrib import admin
from django.contrib.admin.models import DELETION
from django.contrib.contenttypes.models import ContentType
from django.urls import reverse, NoReverseMatch
from django.utils.encoding import force_str
from django.utils.html import escape
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _
from django.contrib.admin.models import DELETION # 操作类型:删除
from django.contrib.contenttypes.models import ContentType # 内容类型模型
from django.urls import reverse, NoReverseMatch # URL反向解析相关
from django.utils.encoding import force_str # 字符串编码处理
from django.utils.html import escape # HTML转义
from django.utils.safestring import mark_safe # 标记安全HTML内容
from django.utils.translation import gettext_lazy as _ # 国际化翻译
# 自定义后台操作日志管理类控制LogEntry模型在后台的展示与操作
class LogEntryAdmin(admin.ModelAdmin):
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',
]
# 侧边栏筛选条件:按内容类型筛选
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
# 限制修改权限仅超级用户或有权限用户可查看禁止POST提交修改
def has_change_permission(self, request, obj=None):
return (
request.user.is_superuser or
request.user.has_perm('admin.change_logentry')
) and request.method != '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)
object_link = escape(obj.object_repr) # 转义对象描述避免XSS
content_type = obj.content_type
# 非删除操作且有内容类型时,尝试生成对象的后台编辑链接
if obj.action_flag != DELETION and content_type is not None:
# try returning an actual link instead of object repr string
try:
url = reverse(
'admin:{}_{}_change'.format(content_type.app_label,
content_type.model),
args=[obj.object_id]
)
object_link = '<a href="{}">{}</a>'.format(url, object_link)
except NoReverseMatch:
url = reverse(f'admin:{content_type.app_label}_{content_type.model}_change', args=[obj.object_id])
object_link = f'<a href=" ">{object_link}</a >'
except NoReverseMatch: # 无法解析URL时仅显示文本
pass
return mark_safe(object_link)
return mark_safe(object_link) # 标记为安全HTML允许页面渲染链接
object_link.admin_order_field = 'object_repr'
object_link.short_description = _('object')
object_link.admin_order_field = 'object_repr' # 支持按对象描述排序
object_link.short_description = _('object') # 字段显示名称
# 生成操作用户的后台编辑链接
def user_link(self, obj):
content_type = ContentType.objects.get_for_model(type(obj.user))
user_link = escape(force_str(obj.user))
content_type = ContentType.objects.get_for_model(type(obj.user)) # 获取用户模型的内容类型
user_link = escape(force_str(obj.user)) # 转义用户名
try:
# try returning an actual link instead of object repr string
url = reverse(
'admin:{}_{}_change'.format(content_type.app_label,
content_type.model),
args=[obj.user.pk]
)
user_link = '<a href="{}">{}</a>'.format(url, user_link)
# 生成用户的后台编辑URL
url = reverse(f'admin:{content_type.app_label}_{content_type.model}_change', args=[obj.user.pk])
user_link = f'<a href="{url}">{user_link}</a >'
except NoReverseMatch:
pass
return mark_safe(user_link)
user_link.admin_order_field = 'user'
user_link.short_description = _('user')
user_link.admin_order_field = 'user' # 支持按用户排序
user_link.short_description = _('user') # 字段显示名称
# 优化查询预加载content_type减少数据库查询次数
def get_queryset(self, request):
queryset = super(LogEntryAdmin, self).get_queryset(request)
queryset = super().get_queryset(request)
return queryset.prefetch_related('content_type')
# 移除批量删除操作:避免误删日志
def get_actions(self, request):
actions = super(LogEntryAdmin, self).get_actions(request)
actions = super().get_actions(request)
if 'delete_selected' in actions:
del actions['delete_selected']
return actions
return actions

@ -1,41 +0,0 @@
import logging
logger = logging.getLogger(__name__)
class BasePlugin:
# 插件元数据
PLUGIN_NAME = None
PLUGIN_DESCRIPTION = None
PLUGIN_VERSION = None
def __init__(self):
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):
"""
插件初始化逻辑
子类可以重写此方法来实现特定的初始化操作
"""
logger.info(f'{self.PLUGIN_NAME} initialized.')
def register_hooks(self):
"""
注册插件钩子
子类可以重写此方法来注册特定的钩子
"""
pass
def get_plugin_info(self):
"""
获取插件信息
:return: 包含插件元数据的字典
"""
return {
'name': self.PLUGIN_NAME,
'description': self.PLUGIN_DESCRIPTION,
'version': self.PLUGIN_VERSION
}

@ -1,7 +0,0 @@
ARTICLE_DETAIL_LOAD = 'article_detail_load'
ARTICLE_CREATE = 'article_create'
ARTICLE_UPDATE = 'article_update'
ARTICLE_DELETE = 'article_delete'
ARTICLE_CONTENT_HOOK_NAME = "the_content"

@ -1,44 +0,0 @@
import logging
logger = logging.getLogger(__name__)
_hooks = {}
def register(hook_name: str, callback: callable):
"""
注册一个钩子回调
"""
if hook_name not in _hooks:
_hooks[hook_name] = []
_hooks[hook_name].append(callback)
logger.debug(f"Registered hook '{hook_name}' with callback '{callback.__name__}'")
def run_action(hook_name: str, *args, **kwargs):
"""
执行一个 Action Hook
它会按顺序执行所有注册到该钩子上的回调函数
"""
if hook_name in _hooks:
logger.debug(f"Running action hook '{hook_name}'")
for callback in _hooks[hook_name]:
try:
callback(*args, **kwargs)
except Exception as e:
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
它会把 value 依次传递给所有注册的回调函数进行处理
"""
if hook_name in _hooks:
logger.debug(f"Applying filter hook '{hook_name}'")
for callback in _hooks[hook_name]:
try:
value = callback(value, *args, **kwargs)
except Exception as e:
logger.error(f"Error applying filter hook '{hook_name}' callback '{callback.__name__}': {e}", exc_info=True)
return value

@ -1,19 +0,0 @@
import os
import logging
from django.conf import settings
logger = logging.getLogger(__name__)
def load_plugins():
"""
Dynamically loads and initializes plugins from the 'plugins' directory.
This function is intended to be called when the Django app registry is ready.
"""
for plugin_name in settings.ACTIVE_PLUGINS:
plugin_path = os.path.join(settings.PLUGINS_DIR, plugin_name)
if os.path.isdir(plugin_path) and os.path.exists(os.path.join(plugin_path, 'plugin.py')):
try:
__import__(f'plugins.{plugin_name}.plugin')
logger.info(f"Successfully loaded plugin: {plugin_name}")
except ImportError as e:
logger.error(f"Failed to import plugin: {plugin_name}", exc_info=e)

@ -1,341 +1,77 @@
"""
Django settings for djangoblog project.
Generated by 'django-admin startproject' using Django 1.10.2.
For more information on this file, see
https://docs.djangoproject.com/en/1.10/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/1.10/ref/settings/
"""
import os
import sys
from pathlib import Path
from django.utils.translation import gettext_lazy as _
def env_to_bool(env, default):
str_val = os.environ.get(env)
return default if str_val is None else str_val == 'True'
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = os.environ.get(
'DJANGO_SECRET_KEY') or 'n9ceqv38)#&mwuat@(mjb_p%em$e8$qyr#fw9ot!=ba6lijx-6'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = env_to_bool('DJANGO_DEBUG', True)
# DEBUG = False
TESTING = len(sys.argv) > 1 and sys.argv[1] == 'test'
# ALLOWED_HOSTS = []
ALLOWED_HOSTS = ['*', '127.0.0.1', 'example.com']
# django 4.0新增配置
CSRF_TRUSTED_ORIGINS = ['http://example.com']
# Application definition
INSTALLED_APPS = [
# 'django.contrib.admin',
'django.contrib.admin.apps.SimpleAdminConfig',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django.contrib.sites',
'django.contrib.sitemaps',
'mdeditor',
'haystack',
'blog',
'accounts',
'comments',
'oauth',
'servermanager',
'owntracks',
'compressor',
'djangoblog'
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.locale.LocaleMiddleware',
'django.middleware.gzip.GZipMiddleware',
# 'django.middleware.cache.UpdateCacheMiddleware',
'django.middleware.common.CommonMiddleware',
# 'django.middleware.cache.FetchFromCacheMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'django.middleware.http.ConditionalGetMiddleware',
'blog.middleware.OnlineMiddleware'
]
ROOT_URLCONF = 'djangoblog.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR, 'templates')],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
'blog.context_processors.seo_processor'
],
},
},
]
WSGI_APPLICATION = 'djangoblog.wsgi.application'
# Database
# https://docs.djangoproject.com/en/1.10/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'djangoblog',
'USER': 'root',
'PASSWORD': '123456',
'HOST': '127.0.0.1',
'PORT': 3306,
}
}
# Password validation
# https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'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'
TIME_ZONE = 'Asia/Shanghai'
USE_I18N = True
USE_L10N = True
USE_TZ = False
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.10/howto/static-files/
HAYSTACK_CONNECTIONS = {
'default': {
'ENGINE': 'djangoblog.whoosh_cn_backend.WhooshEngine',
'PATH': os.path.join(os.path.dirname(__file__), 'whoosh_index'),
},
}
# Automatically update searching index
HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor'
# Allow user login with username and password
AUTHENTICATION_BACKENDS = [
'accounts.user_login_backend.EmailOrUsernameModelBackend']
STATIC_ROOT = os.path.join(BASE_DIR, 'collectedstatic')
STATIC_URL = '/static/'
STATICFILES = os.path.join(BASE_DIR, 'static')
AUTH_USER_MODEL = 'accounts.BlogUser'
LOGIN_URL = '/login/'
TIME_FORMAT = '%Y-%m-%d %H:%M:%S'
DATE_TIME_FORMAT = '%Y-%m-%d'
# bootstrap color styles
BOOTSTRAP_COLOR_TYPES = [
'default', 'primary', 'success', 'info', 'warning', 'danger'
]
# paginate
PAGINATE_BY = 10
# http cache timeout
CACHE_CONTROL_MAX_AGE = 2592000
# cache setting
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
'TIMEOUT': 10800,
'LOCATION': 'unique-snowflake',
}
}
# 使用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")}',
}
}
SITE_ID = 1
BAIDU_NOTIFY_URL = os.environ.get('DJANGO_BAIDU_NOTIFY_URL') \
or 'http://data.zz.baidu.com/urls?site=https://www.lylinux.net&token=1uAOGrMsUm5syDGn'
# Email:
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_USE_TLS = env_to_bool('DJANGO_EMAIL_TLS', False)
EMAIL_USE_SSL = env_to_bool('DJANGO_EMAIL_SSL', True)
EMAIL_HOST = os.environ.get('DJANGO_EMAIL_HOST') or 'smtp.mxhichina.com'
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')
DEFAULT_FROM_EMAIL = EMAIL_HOST_USER
SERVER_EMAIL = EMAIL_HOST_USER
# Setting debug=false did NOT handle except email notifications
ADMINS = [('admin', os.environ.get('DJANGO_ADMIN_EMAIL') or 'admin@admin.com')]
# WX ADMIN password(Two times md5)
WXADMIN = os.environ.get(
'DJANGO_WXADMIN_PASSWORD') or '995F03AC401D6CABABAEF756FC4D43C7'
LOG_PATH = os.path.join(BASE_DIR, 'logs')
if not os.path.exists(LOG_PATH):
os.makedirs(LOG_PATH, exist_ok=True)
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'root': {
'level': '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',
},
'require_debug_true': {
'()': 'django.utils.log.RequireDebugTrue',
},
},
'handlers': {
'log_file': {
'level': 'INFO',
'class': 'logging.handlers.TimedRotatingFileHandler',
'filename': os.path.join(LOG_PATH, 'djangoblog.log'),
'when': 'D',
'formatter': 'verbose',
'interval': 1,
'delay': True,
'backupCount': 5,
'encoding': 'utf-8'
},
'console': {
'level': 'DEBUG',
'filters': ['require_debug_true'],
'class': 'logging.StreamHandler',
'formatter': 'verbose'
},
'null': {
'class': 'logging.NullHandler',
},
'mail_admins': {
'level': 'ERROR',
'filters': ['require_debug_false'],
'class': 'django.utils.log.AdminEmailHandler'
}
},
'loggers': {
'djangoblog': {
'handlers': ['log_file', 'console'],
'level': 'INFO',
'propagate': True,
},
'django.request': {
'handlers': ['mail_admins'],
'level': 'ERROR',
'propagate': False,
}
}
}
STATICFILES_FINDERS = (
'django.contrib.staticfiles.finders.FileSystemFinder',
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
# other
'compressor.finders.CompressorFinder',
)
COMPRESS_ENABLED = True
# COMPRESS_OFFLINE = True
COMPRESS_CSS_FILTERS = [
# creates absolute urls from relative ones
'compressor.filters.css_default.CssAbsoluteFilter',
# css minimizer
'compressor.filters.cssmin.CSSMinFilter'
]
COMPRESS_JS_FILTERS = [
'compressor.filters.jsmin.JSMinFilter'
]
MEDIA_ROOT = os.path.join(BASE_DIR, 'uploads')
MEDIA_URL = '/media/'
X_FRAME_OPTIONS = 'SAMEORIGIN'
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
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',
},
}
# Plugin System
PLUGINS_DIR = BASE_DIR / 'plugins'
ACTIVE_PLUGINS = [
'article_copyright',
'reading_time',
'external_links',
'view_count',
'seo_optimizer'
]
# 导入Django后台管理及相关工具类
from django.contrib import admin
from django.contrib.admin.models import DELETION # 操作类型:删除
from django.contrib.contenttypes.models import ContentType # 内容类型模型
from django.urls import reverse, NoReverseMatch # URL反向解析相关
from django.utils.encoding import force_str # 字符串编码处理
from django.utils.html import escape # HTML转义
from django.utils.safestring import mark_safe # 标记安全HTML内容
from django.utils.translation import gettext_lazy as _ # 国际化翻译
# 自定义后台操作日志管理类控制LogEntry模型在后台的展示与操作
class LogEntryAdmin(admin.ModelAdmin):
# 侧边栏筛选条件:按内容类型筛选
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
# 限制修改权限仅超级用户或有权限用户可查看禁止POST提交修改
def has_change_permission(self, request, obj=None):
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:
url = reverse(f'admin:{content_type.app_label}_{content_type.model}_change', args=[obj.object_id])
object_link = f'<a href=" ">{object_link}</a >'
except NoReverseMatch: # 无法解析URL时仅显示文本
pass
return mark_safe(object_link) # 标记为安全HTML允许页面渲染链接
object_link.admin_order_field = 'object_repr' # 支持按对象描述排序
object_link.short_description = _('object') # 字段显示名称
# 生成操作用户的后台编辑链接
def user_link(self, obj):
content_type = ContentType.objects.get_for_model(type(obj.user)) # 获取用户模型的内容类型
user_link = escape(force_str(obj.user)) # 转义用户名
try:
# 生成用户的后台编辑URL
url = reverse(f'admin:{content_type.app_label}_{content_type.model}_change', args=[obj.user.pk])
user_link = f'<a href="{url}">{user_link}</a >'
except NoReverseMatch:
pass
return mark_safe(user_link)
user_link.admin_order_field = 'user' # 支持按用户排序
user_link.short_description = _('user') # 字段显示名称
# 优化查询预加载content_type减少数据库查询次数
def get_queryset(self, request):
queryset = super().get_queryset(request)
return queryset.prefetch_related('content_type')
# 移除批量删除操作:避免误删日志
def get_actions(self, request):
actions = super().get_actions(request)
if 'delete_selected' in actions:
del actions['delete_selected']
return actions

@ -5,55 +5,76 @@ from blog.models import Article, Category, Tag
class StaticViewSitemap(Sitemap):
priority = 0.5
changefreq = 'daily'
"""静态视图站点地图类,用于处理没有对应模型数据的固定页面[1,2](@ref)"""
priority = 0.5 # 优先级取值范围0.0-1.0默认0.5[2,4](@ref)
changefreq = 'daily' # 内容更新频率可选值always/hourly/daily/weekly/monthly/yearly/never[2,6](@ref)
def items(self):
return ['blog:index', ]
"""返回要包含在站点地图中的URL名称列表[1,2](@ref)"""
return ['blog:index', ] # 这里只包含博客首页,可以添加其他静态页面如'about'、'contact'等
def location(self, item):
return reverse(item)
"""根据URL名称生成完整的URL路径[1,2](@ref)"""
return reverse(item) # 使用Django的reverse函数通过URL名称生成实际URL
class ArticleSiteMap(Sitemap):
changefreq = "monthly"
priority = "0.6"
"""文章模型站点地图类,用于生成所有文章的站点地图条目[1,3](@ref)"""
changefreq = "monthly" # 文章内容通常每月更新
priority = "0.6" # 文章页面优先级较高,因为包含重要内容
def items(self):
return Article.objects.filter(status='p')
"""返回所有已发布的文章对象[3,5](@ref)"""
return Article.objects.filter(status='p') # 只筛选状态为'p'(已发布)的文章
def lastmod(self, obj):
return obj.last_modify_time
"""返回文章的最后修改时间[1,3](@ref)"""
return obj.last_modify_time # 使用文章的last_modify_time字段作为最后修改时间
class CategorySiteMap(Sitemap):
changefreq = "Weekly"
priority = "0.6"
"""分类模型站点地图类,用于生成所有分类页面的站点地图[3](@ref)"""
changefreq = "Weekly" # 分类页面内容相对稳定,每周检查更新
priority = "0.6" # 分类页面有中等优先级
def items(self):
return Category.objects.all()
"""返回所有分类对象[3](@ref)"""
return Category.objects.all() # 包含所有分类
def lastmod(self, obj):
return obj.last_modify_time
"""返回分类的最后修改时间[3](@ref)"""
return obj.last_modify_time # 使用分类的last_modify_time字段
class TagSiteMap(Sitemap):
changefreq = "Weekly"
priority = "0.3"
"""标签模型站点地图类,用于生成所有标签页面的站点地图[3](@ref)"""
changefreq = "Weekly" # 标签页面更新频率为每周
priority = "0.3" # 标签页面优先级较低
def items(self):
return Tag.objects.all()
"""返回所有标签对象[3](@ref)"""
return Tag.objects.all() # 包含所有标签
def lastmod(self, obj):
return obj.last_modify_time
"""返回标签的最后修改时间"""
return obj.last_modify_time # 使用标签的last_modify_time字段
class UserSiteMap(Sitemap):
changefreq = "Weekly"
priority = "0.3"
"""用户站点地图类,用于生成用户相关页面的站点地图"""
changefreq = "Weekly" # 用户信息相对稳定,每周检查
priority = "0.3" # 用户页面优先级较低
def items(self):
"""返回所有有文章的作者用户[7](@ref)"""
# 通过文章获取所有不重复的作者,确保只包含有文章的用户
return list(set(map(lambda x: x.author, Article.objects.all())))
def lastmod(self, obj):
return obj.date_joined
"""返回用户的注册时间[7](@ref)"""
return obj.date_joined # 使用用户的date_joined字段作为最后修改时间

@ -1,21 +1,55 @@
import logging
import requests
from django.conf import settings
# 获取当前模块的日志记录器,用于记录日志信息
logger = logging.getLogger(__name__)
class SpiderNotify():
"""蜘蛛通知类用于向搜索引擎推送URL帮助搜索引擎发现和收录网站内容"""
@staticmethod
def baidu_notify(urls):
"""
向百度搜索引擎推送URL促进网站收录
Args:
urls (list): 需要推送的URL列表通常为新发布或更新的文章链接
Note:
使用百度站长平台的API接口进行URL推送
推送格式为每行一个URL的纯文本数据
"""
try:
# 将URL列表转换为百度API要求的格式每行一个URL的字符串
# 例如:['http://example.com/1', 'http://example.com/2'] -> "http://example.com/1\nhttp://example.com/2"
data = '\n'.join(urls)
# 发送POST请求到百度推送接口[8,9](@ref)
# 使用Django设置中配置的百度推送URL避免硬编码
result = requests.post(settings.BAIDU_NOTIFY_URL, data=data)
# 记录推送结果到日志,便于监控和调试[6,7](@ref)
logger.info(result.text)
except Exception as e:
# 捕获并记录所有可能的异常,如网络错误、配置错误等
# 使用错误级别日志记录异常信息[6](@ref)
logger.error(e)
@staticmethod
def notify(url):
SpiderNotify.baidu_notify(url)
"""
推送URL的便捷方法可以扩展支持多个搜索引擎
Args:
url (str or list): 单个URL字符串或URL列表
"""
# 如果传入的是单个URL转换为列表形式
if isinstance(url, str):
url = [url]
# 调用百度推送方法
# 这里的设计便于未来扩展其他搜索引擎的推送功能
SpiderNotify.baidu_notify(url)

@ -1,32 +1,29 @@
from django.test import TestCase
from django.test import TestCase # 导入Django测试框架的核心类
from djangoblog.utils import *
from djangoblog.utils import * # 导入需要测试的工具函数
class DjangoBlogTest(TestCase):
"""Django博客工具函数测试类继承自TestCase以获得Django测试框架的全部功能"""
def setUp(self):
"""
测试前置设置方法在每个测试方法执行前自动调用
用于初始化测试环境如创建测试数据配置设置等
当前测试不需要特殊设置所以使用pass跳过
"""
pass
# 如果需要,可以在这里创建测试用的模型实例或设置测试环境
# 例如self.user = User.objects.create_user(username='testuser', password='testpass')
def test_utils(self):
md5 = get_sha256('test')
self.assertIsNotNone(md5)
"""测试工具函数的核心测试方法,包含多个工具函数的验证"""
# 测试SHA256哈希函数
md5 = get_sha256('test') # 对'test'字符串进行SHA256加密
self.assertIsNotNone(md5) # 断言结果不为None验证函数正常工作
# 更完整的测试可以添加self.assertEqual(len(md5), 64) # SHA256结果应为64字符
# 测试CommonMarkdown的Markdown解析功能
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)
# Title1 # 一级标题

@ -1,64 +1,106 @@
"""djangoblog URL Configuration
"""
djangoblog URL Configuration
URL配置是Django网站的目录本质是URL与视图函数之间的映射表
通过此文件告诉Django对于哪个URL调用哪段代码
The `urlpatterns` list routes URLs to views. For more information please see:
配置说明文档
https://docs.djangoproject.com/en/1.10/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: url(r'^$', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.conf.urls import url, include
2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls'))
示例
函数视图
1. 导入视图from my_app import views
2. 添加URL模式url(r'^$', views.home, name='home')
基于类的视图
1. 导入视图from other_app.views import Home
2. 添加URL模式url(r'^$', Home.as_view(), name='home')
包含其他URL配置
1. 导入include函数from django.conf.urls import url, include
2. 添加URL模式url(r'^blog/', include('blog.urls'))
"""
from django.conf import settings
from django.conf.urls.i18n import i18n_patterns
from django.conf.urls.static import static
from django.contrib.sitemaps.views import sitemap
from django.conf.urls.i18n import i18n_patterns # 国际化URL模式支持
from django.conf.urls.static import static # 静态文件服务
from django.contrib.sitemaps.views import sitemap # 站点地图视图
from django.urls import path, include
from django.urls import re_path
from haystack.views import search_view_factory
from django.urls import re_path # 正则表达式URL匹配
from haystack.views import search_view_factory # Haystack搜索视图工厂
# 导入自定义模块
from blog.views import EsSearchView
from djangoblog.admin_site import admin_site
from djangoblog.admin_site import admin_site # 自定义admin站点
from djangoblog.elasticsearch_backend import ElasticSearchModelSearchForm
from djangoblog.feeds import DjangoBlogFeed
from djangoblog.feeds import DjangoBlogFeed # RSS订阅源
from djangoblog.sitemap import ArticleSiteMap, CategorySiteMap, StaticViewSitemap, TagSiteMap, UserSiteMap
# 站点地图配置字典,定义不同类型的站点地图[1,3](@ref)
sitemaps = {
'blog': ArticleSiteMap,
'Category': CategorySiteMap,
'Tag': TagSiteMap,
'User': UserSiteMap,
'static': StaticViewSitemap
'blog': ArticleSiteMap, # 文章站点地图
'Category': CategorySiteMap, # 分类站点地图
'Tag': TagSiteMap, # 标签站点地图
'User': UserSiteMap, # 用户站点地图
'static': StaticViewSitemap # 静态页面站点地图
}
handler404 = 'blog.views.page_not_found_view'
handler500 = 'blog.views.server_error_view'
handle403 = 'blog.views.permission_denied_view'
# 自定义错误处理视图[2](@ref)
handler404 = 'blog.views.page_not_found_view' # 404页面未找到处理
handler500 = 'blog.views.server_error_view' # 500服务器错误处理
handle403 = 'blog.views.permission_denied_view' # 403权限拒绝处理
# 基础URL模式配置
urlpatterns = [
# 国际化URL支持提供语言切换功能[7,8](@ref)
path('i18n/', include('django.conf.urls.i18n')),
]
# 使用i18n_patterns为URL添加语言前缀支持国际化[6,7](@ref)
urlpatterns += i18n_patterns(
# 管理员后台URL使用自定义的admin_site[4](@ref)
re_path(r'^admin/', admin_site.urls),
# 博客应用URL包含博客相关所有路由[1,4](@ref)
re_path(r'', include('blog.urls', namespace='blog')),
# Markdown编辑器URL[4](@ref)
re_path(r'mdeditor/', include('mdeditor.urls')),
# 评论系统URL[4](@ref)
re_path(r'', include('comments.urls', namespace='comment')),
# 用户账户URL[4](@ref)
re_path(r'', include('accounts.urls', namespace='account')),
# OAuth认证URL[4](@ref)
re_path(r'', include('oauth.urls', namespace='oauth')),
# 站点地图XML文件URL[1](@ref)
re_path(r'^sitemap\.xml$', sitemap, {'sitemaps': sitemaps},
name='django.contrib.sitemaps.views.sitemap'),
# RSS订阅源URLfeed和rss两个端点[1](@ref)
re_path(r'^feed/$', DjangoBlogFeed()),
re_path(r'^rss/$', DjangoBlogFeed()),
# 搜索功能URL使用ElasticSearch作为后端[4](@ref)
re_path('^search', search_view_factory(view_class=EsSearchView, form_class=ElasticSearchModelSearchForm),
name='search'),
# 服务器管理URL[4](@ref)
re_path(r'', include('servermanager.urls', namespace='servermanager')),
re_path(r'', include('owntracks.urls', namespace='owntracks'))
, prefix_default_language=False) + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
# OwnTracks位置跟踪URL[4](@ref)
re_path(r'', include('owntracks.urls', namespace='owntracks')),
# 不强制为默认语言添加前缀[6,7](@ref)
prefix_default_language=False
)
# 静态文件服务配置(开发环境)[2](@ref)
+ static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
# 开发环境下媒体文件服务配置[2](@ref)
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL,
document_root=settings.MEDIA_ROOT)
document_root=settings.MEDIA_ROOT)

@ -1,7 +1,6 @@
#!/usr/bin/env python
# encoding: utf-8
import logging
import os
import random
@ -9,41 +8,61 @@ import string
import uuid
from hashlib import sha256
import bleach
import markdown
import requests
import bleach # HTML清理库用于防止XSS攻击
import markdown # Markdown解析库
import requests # HTTP请求库
from django.conf import settings
from django.contrib.sites.models import Site
from django.core.cache import cache
from django.templatetags.static import static
from django.contrib.sites.models import Site # Django站点框架
from django.core.cache import cache # Django缓存框架
from django.templatetags.static import static # 静态文件URL生成
logger = logging.getLogger(__name__)
def get_max_articleid_commentid():
"""获取最大的文章ID和评论ID"""
from blog.models import Article
from comments.models import Comment
return (Article.objects.latest().pk, Comment.objects.latest().pk)
def get_sha256(str):
"""计算字符串的SHA256哈希值
Args:
str: 要计算哈希的字符串
Returns:
str: 64位的十六进制哈希值
"""
m = sha256(str.encode('utf-8'))
return m.hexdigest()
def cache_decorator(expiration=3 * 60):
"""缓存装饰器,用于缓存函数结果
Args:
expiration: 缓存过期时间默认3分钟
Returns:
function: 装饰器函数
"""
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:
# logger.info('cache_decorator get cache:%s key:%s' % (func.__name__, key))
@ -52,9 +71,8 @@ def cache_decorator(expiration=3 * 60):
else:
return value
else:
logger.debug(
'cache_decorator set cache:%s key:%s' %
(func.__name__, key))
# 缓存未命中,执行函数并缓存结果
logger.debug('cache_decorator set cache:%s key:%s' % (func.__name__, key))
value = func(*args, **kwargs)
if value is None:
cache.set(key, '__default_cache_value__', expiration)
@ -68,21 +86,26 @@ def cache_decorator(expiration=3 * 60):
def expire_view_cache(path, servername, serverport, key_prefix=None):
'''
刷新视图缓存
:param path:url路径
:param servername:host
:param serverport:端口
:param key_prefix:前缀
:return:是否成功
'''刷新视图缓存
Args:
path: URL路径
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('expire_view_cache:get key:{path}'.format(path=path))
@ -92,40 +115,62 @@ def expire_view_cache(path, servername, serverport, key_prefix=None):
return False
@cache_decorator()
@cache_decorator() # 应用缓存装饰器
def get_current_site():
"""获取当前站点信息"""
site = Site.objects.get_current()
return site
class CommonMarkdown:
"""Markdown处理工具类"""
@staticmethod
def _convert_markdown(value):
"""内部方法转换Markdown为HTML
Args:
value: Markdown格式文本
Returns:
tuple: (HTML内容, 目录)
"""
# 配置Markdown扩展
md = markdown.Markdown(
extensions=[
'extra',
'codehilite',
'toc',
'tables',
'extra', # 额外语法支持
'codehilite', # 代码高亮
'toc', # 目录生成
'tables', # 表格支持
]
)
body = md.convert(value)
toc = md.toc
body = md.convert(value) # 转换Markdown为HTML
toc = md.toc # 获取目录
return body, toc
@staticmethod
def get_markdown_with_toc(value):
"""获取带目录的Markdown转换结果"""
body, toc = CommonMarkdown._convert_markdown(value)
return body, toc
@staticmethod
def get_markdown(value):
"""获取Markdown转换结果不含目录"""
body, toc = CommonMarkdown._convert_markdown(value)
return body
def send_email(emailto, title, content):
"""发送邮件(通过信号机制)
Args:
emailto: 收件人邮箱
title: 邮件标题
content: 邮件内容
"""
from djangoblog.blog_signals import send_email_signal
# 使用Django信号发送邮件实现解耦[9,10,11](@ref)
send_email_signal.send(
send_email.__class__,
emailto=emailto,
@ -134,11 +179,19 @@ def send_email(emailto, title, content):
def generate_code() -> str:
"""生成随机数验证码"""
"""生成6位随机数验证码"""
return ''.join(random.sample(string.digits, 6))
def parse_dict_to_url(dict):
"""将字典转换为URL参数字符串
Args:
dict: 参数字典
Returns:
str: URL参数字符串
"""
from urllib.parse import quote
url = '&'.join(['{}={}'.format(quote(k, safe='/'), quote(v, safe='/'))
for k, v in dict.items()])
@ -146,12 +199,19 @@ def parse_dict_to_url(dict):
def get_blog_setting():
"""获取博客设置,使用缓存提高性能[6,8](@ref)
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的博客系统'
@ -169,39 +229,44 @@ def get_blog_setting():
setting.save()
value = BlogSettings.objects.first()
logger.info('set cache get_blog_setting')
cache.set('get_blog_setting', value)
cache.set('get_blog_setting', value) # 设置缓存
return value
def save_user_avatar(url):
'''
保存用户头像
:param url:头像url
:return: 本地路径
'''保存用户头像到本地
Args:
url: 头像URL地址
Returns:
str: 本地静态文件路径
'''
logger.info(url)
try:
basedir = os.path.join(settings.STATICFILES, 'avatar')
rsp = requests.get(url, timeout=2)
rsp = requests.get(url, timeout=2) # 下载头像
if rsp.status_code == 200:
if not os.path.exists(basedir):
os.makedirs(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'
save_filename = str(uuid.uuid4().hex) + ext
save_filename = str(uuid.uuid4().hex) + ext # 生成唯一文件名
logger.info('保存用户头像:' + basedir + save_filename)
with open(os.path.join(basedir, save_filename), 'wb+') as file:
file.write(rsp.content)
return static('avatar/' + save_filename)
file.write(rsp.content) # 保存文件
return static('avatar/' + save_filename) # 返回静态文件URL
except Exception as e:
logger.error(e)
return static('blog/img/avatar.png')
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:
@ -210,12 +275,19 @@ def delete_sidebar_cache():
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基础路径"""
if settings.STATIC_URL:
return settings.STATIC_URL
else:
@ -223,10 +295,19 @@ def get_resource_url():
return 'http://' + site.domain + '/static/'
ALLOWED_TAGS = ['a', 'abbr', 'acronym', 'b', 'blockquote', 'code', 'em', 'i', 'li', 'ol', 'pre', 'strong', 'ul', 'h1',
'h2', 'p']
# 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):
return bleach.clean(html, tags=ALLOWED_TAGS, attributes=ALLOWED_ATTRIBUTES)
"""清理HTML移除不安全的标签和属性
Args:
html: 要清理的HTML内容
Returns:
str: 安全的HTML内容
"""
return bleach.clean(html, tags=ALLOWED_TAGS, attributes=ALLOWED_ATTRIBUTES)

@ -1,16 +1,22 @@
"""
WSGI config for djangoblog project.
It exposes the WSGI callable as a module-level variable named ``application``.
WSGIWeb Server Gateway Interface是Python的Web服务器网关接口是Django的主要部署平台
这个文件包含了WSGI可调用对象作为模块级别的变量名为`application`
For more information on this file, see
更多关于此文件的信息请参考
https://docs.djangoproject.com/en/1.10/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
# 设置Django的默认设置模块环境变量
# DJANGO_SETTINGS_MODULE环境变量告诉Django应该使用哪个设置模块
# 当WSGI服务器加载应用时Django需要知道使用哪个设置文件来配置整个应用[6,7](@ref)
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "djangoblog.settings")
application = get_wsgi_application()
# 获取WSGI可调用应用程序对象
# 这个application对象是WSGI服务器与Django应用通信的接口[6,9](@ref)
# get_wsgi_application()函数返回一个符合WSGI标准的可调用应用程序对象
application = get_wsgi_application()

@ -1,22 +0,0 @@
#!/usr/bin/env python
import os
import sys
if __name__ == "__main__":
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "djangoblog.settings")
try:
from django.core.management import execute_from_command_line
except ImportError:
# The above import may fail for some other reason. Ensure that the
# issue is really that Django is missing to avoid masking other
# exceptions on Python 2.
try:
import django
except ImportError:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
)
raise
execute_from_command_line(sys.argv)

@ -8,14 +8,20 @@ from django.utils.translation import gettext_lazy as _
from .models import BlogUser
# xm: 自定义用户创建表单继承自ModelForm
class BlogUserCreationForm(forms.ModelForm):
# xm: 密码输入字段1
password1 = forms.CharField(label=_('password'), widget=forms.PasswordInput)
# xm: 密码确认字段2
password2 = forms.CharField(label=_('Enter password again'), widget=forms.PasswordInput)
class Meta:
# xm: 指定关联的模型为BlogUser
model = BlogUser
# xm: 表单字段只包含email
fields = ('email',)
# xm: 密码验证方法,确保两次输入的密码一致
def clean_password2(self):
# Check that the two password entries match
password1 = self.cleaned_data.get("password1")
@ -24,29 +30,40 @@ class BlogUserCreationForm(forms.ModelForm):
raise forms.ValidationError(_("passwords do not match"))
return password2
# xm: 保存用户信息,对密码进行哈希处理
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:
# xm: 设置用户来源为管理员站点
user.source = 'adminsite'
user.save()
return user
# xm: 自定义用户信息修改表单继承自UserChangeForm
class BlogUserChangeForm(UserChangeForm):
class Meta:
# xm: 指定关联的模型为BlogUser
model = BlogUser
# xm: 包含所有字段
fields = '__all__'
# xm: 指定username字段使用UsernameField类型
field_classes = {'username': UsernameField}
# xm: 初始化方法
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# xm: 自定义用户管理类继承自UserAdmin
class BlogUserAdmin(UserAdmin):
# xm: 指定修改表单类
form = BlogUserChangeForm
# xm: 指定创建表单类
add_form = BlogUserCreationForm
# xm: 列表页面显示的字段
list_display = (
'id',
'nickname',
@ -55,5 +72,7 @@ class BlogUserAdmin(UserAdmin):
'last_login',
'date_joined',
'source')
# xm: 列表页面可点击链接的字段
list_display_links = ('id', 'username')
# xm: 默认排序字段按ID倒序
ordering = ('-id',)

@ -0,0 +1,5 @@
from django.apps import AppConfig
# xm: 应用配置类继承自Django的AppConfig基类
class AccountsConfig(AppConfig):
# xm: 指定应用的完整Python路径
name = 'accounts'

@ -8,28 +8,37 @@ from . import utils
from .models import BlogUser
# xm: 自定义登录表单继承自Django的AuthenticationForm
class LoginForm(AuthenticationForm):
def __init__(self, *args, **kwargs):
super(LoginForm, self).__init__(*args, **kwargs)
# xm: 设置用户名字段的widget添加placeholder和CSS类
self.fields['username'].widget = widgets.TextInput(
attrs={'placeholder': "username", "class": "form-control"})
# xm: 设置密码字段的widget添加placeholder和CSS类
self.fields['password'].widget = widgets.PasswordInput(
attrs={'placeholder': "password", "class": "form-control"})
# xm: 自定义用户注册表单继承自Django的UserCreationForm
class RegisterForm(UserCreationForm):
def __init__(self, *args, **kwargs):
super(RegisterForm, self).__init__(*args, **kwargs)
# xm: 设置用户名字段的widget添加placeholder和CSS类
self.fields['username'].widget = widgets.TextInput(
attrs={'placeholder': "username", "class": "form-control"})
# xm: 设置邮箱字段的widget添加placeholder和CSS类
self.fields['email'].widget = widgets.EmailInput(
attrs={'placeholder': "email", "class": "form-control"})
# xm: 设置密码字段的widget添加placeholder和CSS类
self.fields['password1'].widget = widgets.PasswordInput(
attrs={'placeholder': "password", "class": "form-control"})
# xm: 设置密码确认字段的widget添加placeholder和CSS类
self.fields['password2'].widget = widgets.PasswordInput(
attrs={'placeholder': "repeat password", "class": "form-control"})
# xm: 邮箱验证方法,确保邮箱唯一性
def clean_email(self):
email = self.cleaned_data['email']
if get_user_model().objects.filter(email=email).exists():
@ -37,11 +46,15 @@ class RegisterForm(UserCreationForm):
return email
class Meta:
# xm: 指定关联的用户模型
model = get_user_model()
# xm: 表单包含的字段:用户名和邮箱
fields = ("username", "email")
# xm: 忘记密码表单继承自forms.Form
class ForgetPasswordForm(forms.Form):
# xm: 新密码输入字段
new_password1 = forms.CharField(
label=_("New password"),
widget=forms.PasswordInput(
@ -52,6 +65,7 @@ class ForgetPasswordForm(forms.Form):
),
)
# xm: 确认新密码字段
new_password2 = forms.CharField(
label="确认密码",
widget=forms.PasswordInput(
@ -62,6 +76,7 @@ class ForgetPasswordForm(forms.Form):
),
)
# xm: 邮箱输入字段,用于验证用户身份
email = forms.EmailField(
label='邮箱',
widget=forms.TextInput(
@ -72,6 +87,7 @@ class ForgetPasswordForm(forms.Form):
),
)
# xm: 验证码输入字段
code = forms.CharField(
label=_('Code'),
widget=forms.TextInput(
@ -82,15 +98,17 @@ class ForgetPasswordForm(forms.Form):
),
)
# xm: 验证两次输入的新密码是否一致
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"))
# xm: 使用Django的密码验证器验证密码强度
password_validation.validate_password(password2)
return password2
# xm: 验证邮箱是否存在系统中
def clean_email(self):
user_email = self.cleaned_data.get("email")
if not BlogUser.objects.filter(
@ -100,6 +118,7 @@ class ForgetPasswordForm(forms.Form):
raise ValidationError(_("email does not exist"))
return user_email
# xm: 验证验证码是否正确
def clean_code(self):
code = self.cleaned_data.get("code")
error = utils.verify(
@ -111,7 +130,9 @@ class ForgetPasswordForm(forms.Form):
return code
# xm: 获取忘记密码验证码的表单
class ForgetPasswordCodeForm(forms.Form):
# xm: 邮箱字段,用于发送验证码
email = forms.EmailField(
label=_('Email'),
)

@ -18,30 +18,52 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='BlogUser',
fields=[
# xm: 主键ID自增BigAutoField
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
# xm: 密码字段,存储加密后的密码
('password', models.CharField(max_length=128, verbose_name='password')),
# xm: 最后登录时间,记录用户最后一次登录的时间
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
# xm: 超级用户标志,标记用户是否拥有所有权限
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
# xm: 用户名,唯一且需要符合验证器规则
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
# xm: 名字字段,可选
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
# xm: 姓氏字段,可选
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
# xm: 邮箱地址,可选
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
# xm: 职员状态,标记用户是否可以访问管理后台
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
# xm: 活跃状态,标记用户账号是否激活
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
# xm: 加入日期,记录用户注册时间
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
# xm: 昵称字段,博客用户特有属性
('nickname', models.CharField(blank=True, max_length=100, verbose_name='昵称')),
# xm: 创建时间,记录用户账号创建时间
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
# xm: 最后修改时间,记录用户信息最后修改时间
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
# xm: 创建来源,记录用户注册来源
('source', models.CharField(blank=True, max_length=100, verbose_name='创建来源')),
# xm: 用户组多对多关系
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
# xm: 用户权限多对多关系
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
],
options={
# xm: 单数名称显示
'verbose_name': '用户',
# xm: 复数名称显示
'verbose_name_plural': '用户',
# xm: 默认按ID倒序排列
'ordering': ['-id'],
# xm: 指定最新记录的获取字段
'get_latest_by': 'id',
},
# xm: 指定自定义用户模型的管理器
managers=[
('objects', django.contrib.auth.models.UserManager()),
],

@ -11,33 +11,40 @@ class Migration(migrations.Migration):
]
operations = [
# xm: 修改BlogUser模型的元数据选项
migrations.AlterModelOptions(
name='bloguser',
options={'get_latest_by': 'id', 'ordering': ['-id'], 'verbose_name': 'user', 'verbose_name_plural': 'user'},
),
# xm: 删除旧的created_time字段
migrations.RemoveField(
model_name='bloguser',
name='created_time',
),
# xm: 删除旧的last_mod_time字段
migrations.RemoveField(
model_name='bloguser',
name='last_mod_time',
),
# xm: 添加新的creation_time字段使用当前时间作为默认值
migrations.AddField(
model_name='bloguser',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
# xm: 添加新的last_modify_time字段使用当前时间作为默认值
migrations.AddField(
model_name='bloguser',
name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='last modify time'),
),
# xm: 修改nickname字段的verbose_name为英文
migrations.AlterField(
model_name='bloguser',
name='nickname',
field=models.CharField(blank=True, max_length=100, verbose_name='nick name'),
),
# xm: 修改source字段的verbose_name为英文
migrations.AlterField(
model_name='bloguser',
name='source',

@ -8,20 +8,28 @@ from djangoblog.utils import get_current_site
# Create your models here.
# xm: 自定义用户模型继承自Django的AbstractUser基类
class BlogUser(AbstractUser):
# xm: 昵称字段最大长度100可为空
nickname = models.CharField(_('nick name'), max_length=100, blank=True)
# xm: 创建时间字段,默认值为当前时间
creation_time = models.DateTimeField(_('creation time'), default=now)
# xm: 最后修改时间字段,默认值为当前时间
last_modify_time = models.DateTimeField(_('last modify time'), default=now)
# xm: 用户来源字段,记录创建来源,可为空
source = models.CharField(_('create source'), max_length=100, blank=True)
# xm: 获取用户绝对URL的方法用于生成作者详情页链接
def get_absolute_url(self):
return reverse(
'blog:author_detail', kwargs={
'author_name': self.username})
# xm: 对象的字符串表示形式,返回邮箱地址
def __str__(self):
return self.email
# xm: 获取用户完整URL的方法包含域名
def get_full_url(self):
site = get_current_site().domain
url = "https://{site}{path}".format(site=site,
@ -29,7 +37,11 @@ class BlogUser(AbstractUser):
return url
class Meta:
# xm: 默认按ID倒序排列
ordering = ['-id']
# xm: 单数名称显示
verbose_name = _('user')
# xm: 复数名称显示(与单数相同)
verbose_name_plural = verbose_name
# xm: 指定最新记录的获取字段
get_latest_by = 'id'

@ -11,10 +11,13 @@ from . import utils
# Create your tests here.
# xm: 账户功能测试类继承自Django的TestCase
class AccountTest(TestCase):
# xm: 测试初始化方法,每个测试方法执行前都会运行
def setUp(self):
self.client = Client()
self.factory = RequestFactory()
# xm: 创建测试用户
self.blog_user = BlogUser.objects.create_user(
username="test",
email="admin@admin.com",
@ -22,27 +25,33 @@ class AccountTest(TestCase):
)
self.new_test = "xxx123--="
# xm: 测试账户验证功能
def test_validate_account(self):
site = get_current_site().domain
# xm: 创建超级用户
user = BlogUser.objects.create_superuser(
email="liangliangyy1@gmail.com",
username="liangliangyy1",
password="qwer!@#$ggg")
testuser = BlogUser.objects.get(username='liangliangyy1')
# xm: 测试用户登录功能
loginresult = self.client.login(
username='liangliangyy1',
password='qwer!@#$ggg')
self.assertEqual(loginresult, True)
# xm: 测试管理员页面访问权限
response = self.client.get('/admin/')
self.assertEqual(response.status_code, 200)
# xm: 创建测试分类
category = Category()
category.name = "categoryaaa"
category.creation_time = timezone.now()
category.last_modify_time = timezone.now()
category.save()
# xm: 创建测试文章
article = Article()
article.title = "nicetitleaaa"
article.body = "nicecontentaaa"
@ -52,44 +61,53 @@ class AccountTest(TestCase):
article.status = 'p'
article.save()
# xm: 测试文章管理页面访问权限
response = self.client.get(article.get_admin_url())
self.assertEqual(response.status_code, 200)
# xm: 测试用户注册功能
def test_validate_register(self):
self.assertEquals(
0, len(
BlogUser.objects.filter(
email='user123@user.com')))
# xm: 模拟用户注册请求
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',
})
# xm: 验证用户是否创建成功
self.assertEquals(
1, len(
BlogUser.objects.filter(
email='user123@user.com')))
user = BlogUser.objects.filter(email='user123@user.com')[0]
# xm: 生成验证签名
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)
# xm: 测试验证页面访问
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
# xm: 测试用户登录功能
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()
# xm: 创建测试分类
category = Category()
category.name = "categoryaaa"
category.creation_time = timezone.now()
category.last_modify_time = timezone.now()
category.save()
# xm: 创建测试文章
article = Article()
article.category = category
article.title = "nicetitle333"
@ -100,36 +118,45 @@ class AccountTest(TestCase):
article.status = 'p'
article.save()
# xm: 测试文章管理页面访问权限
response = self.client.get(article.get_admin_url())
self.assertEqual(response.status_code, 200)
# xm: 测试用户登出功能
response = self.client.get(reverse('account:logout'))
self.assertIn(response.status_code, [301, 302, 200])
# xm: 测试登出后访问文章管理页面的重定向
response = self.client.get(article.get_admin_url())
self.assertIn(response.status_code, [301, 302, 200])
# xm: 测试错误密码登录
response = self.client.post(reverse('account:login'), {
'username': 'user1233',
'password': 'password123'
})
self.assertIn(response.status_code, [301, 302, 200])
# xm: 测试登录后访问文章管理页面
response = self.client.get(article.get_admin_url())
self.assertIn(response.status_code, [301, 302, 200])
# xm: 测试邮箱验证码功能
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)
# xm: 测试验证码验证成功情况
err = utils.verify("admin@admin.com", code)
self.assertEqual(err, None)
# xm: 测试验证码验证失败情况
err = utils.verify("admin@123.com", code)
self.assertEqual(type(err), str)
# xm: 测试忘记密码验证码发送成功情况
def test_forget_password_email_code_success(self):
resp = self.client.post(
path=reverse("account:forget_password_code"),
@ -139,6 +166,7 @@ class AccountTest(TestCase):
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.content.decode("utf-8"), "ok")
# xm: 测试忘记密码验证码发送失败情况
def test_forget_password_email_code_fail(self):
resp = self.client.post(
path=reverse("account:forget_password_code"),
@ -152,6 +180,7 @@ class AccountTest(TestCase):
)
self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱")
# xm: 测试忘记密码重置成功情况
def test_forget_password_email_success(self):
code = generate_code()
utils.set_code(self.blog_user.email, code)
@ -167,13 +196,14 @@ class AccountTest(TestCase):
)
self.assertEqual(resp.status_code, 302)
# 验证用户密码是否修改成功
# xm: 验证用户密码是否修改成功
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)
# xm: 测试不存在的用户忘记密码情况
def test_forget_password_email_not_user(self):
data = dict(
new_password1=self.new_test,
@ -188,7 +218,7 @@ class AccountTest(TestCase):
self.assertEqual(resp.status_code, 200)
# xm: 测试验证码错误的忘记密码情况
def test_forget_password_email_code_error(self):
code = generate_code()
utils.set_code(self.blog_user.email, code)

@ -0,0 +1,42 @@
from django.urls import path
from django.urls import re_path
from . import views
from .forms import LoginForm
# xm: 定义应用命名空间为"accounts"
app_name = "accounts"
# xm: 定义URL路由配置列表
urlpatterns = [
# xm: 登录路由使用LoginView视图类指定成功跳转URL和自定义登录表单
re_path(r'^login/$',
views.LoginView.as_view(success_url='/'),
name='login',
kwargs={'authentication_form': LoginForm}),
# xm: 注册路由使用RegisterView视图类指定成功跳转URL
re_path(r'^register/$',
views.RegisterView.as_view(success_url="/"),
name='register'),
# xm: 登出路由使用LogoutView视图类
re_path(r'^logout/$',
views.LogoutView.as_view(),
name='logout'),
# xm: 账户结果页面路由使用函数视图account_result
path(r'account/result.html',
views.account_result,
name='result'),
# xm: 忘记密码路由使用ForgetPasswordView视图类
re_path(r'^forget_password/$',
views.ForgetPasswordView.as_view(),
name='forget_password'),
# xm: 忘记密码验证码路由使用ForgetPasswordEmailCode视图类
re_path(r'^forget_password_code/$',
views.ForgetPasswordEmailCode.as_view(),
name='forget_password_code'),
]

@ -2,25 +2,34 @@ from django.contrib.auth import get_user_model
from django.contrib.auth.backends import ModelBackend
# xm: 自定义认证后端允许使用用户名或邮箱登录继承自ModelBackend
class EmailOrUsernameModelBackend(ModelBackend):
"""
允许使用用户名或邮箱登录
"""
# xm: 用户认证方法,重写父类方法
def authenticate(self, request, username=None, password=None, **kwargs):
# xm: 判断输入是否包含@符号,决定使用邮箱还是用户名查询
if '@' in username:
kwargs = {'email': username}
else:
kwargs = {'username': username}
try:
# xm: 根据用户名或邮箱查询用户
user = get_user_model().objects.get(**kwargs)
# xm: 验证密码是否正确
if user.check_password(password):
return user
# xm: 捕获用户不存在的异常
except get_user_model().DoesNotExist:
return None
# xm: 根据用户ID获取用户对象的方法
def get_user(self, username):
try:
# xm: 通过主键查询用户
return get_user_model().objects.get(pk=username)
# xm: 捕获用户不存在的异常
except get_user_model().DoesNotExist:
return None

@ -10,6 +10,7 @@ from djangoblog.utils import send_email
_code_ttl = timedelta(minutes=5)
# xm: 发送验证邮件函数
def send_verify_email(to_mail: str, code: str, subject: str = _("Verify Email")):
"""发送重设密码验证码
Args:
@ -17,12 +18,15 @@ def send_verify_email(to_mail: str, code: str, subject: str = _("Verify Email"))
subject: 邮件主题
code: 验证码
"""
# xm: 构建邮件HTML内容包含验证码信息
html_content = _(
"You are resetting the password, the verification code is%(code)s, valid within 5 minutes, please keep it "
"properly") % {'code': code}
# xm: 调用发送邮件函数发送验证码
send_email([to_mail], subject, html_content)
# xm: 验证验证码函数
def verify(email: str, code: str) -> typing.Optional[str]:
"""验证code是否有效
Args:
@ -34,16 +38,22 @@ def verify(email: str, code: str) -> typing.Optional[str]:
这里的错误处理不太合理应该采用raise抛出
否测调用方也需要对error进行处理
"""
# xm: 从缓存中获取对应邮箱的验证码
cache_code = get_code(email)
# xm: 比较输入的验证码和缓存中的验证码是否一致
if cache_code != code:
return gettext("Verification code error")
# xm: 设置验证码到缓存函数
def set_code(email: str, code: str):
"""设置code"""
# xm: 使用Django缓存系统存储验证码设置过期时间
cache.set(email, code, _code_ttl.seconds)
# xm: 从缓存获取验证码函数
def get_code(email: str) -> typing.Optional[str]:
"""获取code"""
# xm: 从Django缓存系统中获取指定邮箱的验证码
return cache.get(email)

@ -30,22 +30,26 @@ logger = logging.getLogger(__name__)
# Create your views here.
# xm: 用户注册视图类继承自FormView
class RegisterView(FormView):
form_class = RegisterForm
template_name = 'account/registration_form.html'
# xm: 添加CSRF保护装饰器
@method_decorator(csrf_protect)
def dispatch(self, *args, **kwargs):
return super(RegisterView, self).dispatch(*args, **kwargs)
# xm: 表单验证通过后的处理逻辑
def form_valid(self, form):
if form.is_valid():
# xm: 保存用户但不提交到数据库
user = form.save(False)
user.is_active = False
user.source = 'Register'
user.save(True)
site = get_current_site().domain
# xm: 生成邮箱验证签名
sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
if settings.DEBUG:
@ -54,6 +58,7 @@ class RegisterView(FormView):
url = "http://{site}{path}?type=validation&id={id}&sign={sign}".format(
site=site, path=path, id=user.id, sign=sign)
# xm: 构建验证邮件内容
content = """
<p>请点击下面链接验证您的邮箱</p>
@ -64,6 +69,7 @@ class RegisterView(FormView):
如果上面链接无法打开请将此链接复制至浏览器
{url}
""".format(url=url)
# xm: 发送验证邮件
send_email(
emailto=[
user.email,
@ -71,6 +77,7 @@ class RegisterView(FormView):
title='验证您的电子邮箱',
content=content)
# xm: 重定向到结果页面
url = reverse('accounts:result') + \
'?type=register&id=' + str(user.id)
return HttpResponseRedirect(url)
@ -80,19 +87,24 @@ class RegisterView(FormView):
})
# xm: 用户登出视图类继承自RedirectView
class LogoutView(RedirectView):
url = '/login/'
# xm: 添加不缓存装饰器
@method_decorator(never_cache)
def dispatch(self, request, *args, **kwargs):
return super(LogoutView, self).dispatch(request, *args, **kwargs)
# xm: 处理GET请求执行登出操作
def get(self, request, *args, **kwargs):
logout(request)
# xm: 删除侧边栏缓存
delete_sidebar_cache()
return super(LogoutView, self).get(request, *args, **kwargs)
# xm: 用户登录视图类继承自FormView
class LoginView(FormView):
form_class = LoginForm
template_name = 'account/login.html'
@ -100,41 +112,45 @@ class LoginView(FormView):
redirect_field_name = REDIRECT_FIELD_NAME
login_ttl = 2626560 # 一个月的时间
# xm: 添加多个安全相关的装饰器
@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)
# xm: 获取上下文数据,处理重定向参数
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)
# xm: 表单验证通过后的处理逻辑
def form_valid(self, form):
form = AuthenticationForm(data=self.request.POST, request=self.request)
if form.is_valid():
# xm: 删除侧边栏缓存
delete_sidebar_cache()
logger.info(self.redirect_field_name)
# xm: 执行用户登录
auth.login(self.request, form.get_user())
# xm: 处理"记住我"功能,设置会话过期时间
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
})
# xm: 获取登录成功后的重定向URL
def get_success_url(self):
redirect_to = self.request.POST.get(self.redirect_field_name)
# xm: 验证重定向URL的安全性
if not url_has_allowed_host_and_scheme(
url=redirect_to, allowed_hosts=[
self.request.get_host()]):
@ -142,25 +158,30 @@ class LoginView(FormView):
return redirect_to
# xm: 账户操作结果页面视图函数
def account_result(request):
type = request.GET.get('type')
id = request.GET.get('id')
# xm: 获取用户对象不存在则返回404
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':
# xm: 注册成功页面内容
content = '''
恭喜您注册成功一封验证邮件已经发送到您的邮箱请验证您的邮箱后登录本站
'''
title = '注册成功'
else:
# xm: 验证邮箱签名
c_sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
sign = request.GET.get('sign')
if sign != c_sign:
return HttpResponseForbidden()
# xm: 激活用户账户
user.is_active = True
user.save()
content = '''
@ -175,12 +196,15 @@ def account_result(request):
return HttpResponseRedirect('/')
# xm: 忘记密码视图类继承自FormView
class ForgetPasswordView(FormView):
form_class = ForgetPasswordForm
template_name = 'account/forget_password.html'
# xm: 表单验证通过后的处理逻辑
def form_valid(self, form):
if form.is_valid():
# xm: 根据邮箱获取用户并重置密码
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()
@ -189,14 +213,17 @@ class ForgetPasswordView(FormView):
return self.render_to_response({'form': form})
# xm: 忘记密码验证码发送视图类继承自View
class ForgetPasswordEmailCode(View):
# xm: 处理POST请求发送验证码邮件
def post(self, request: HttpRequest):
form = ForgetPasswordCodeForm(request.POST)
if not form.is_valid():
return HttpResponse("错误的邮箱")
to_email = form.cleaned_data["email"]
# xm: 生成并发送验证码
code = generate_code()
utils.send_verify_email(to_email, code)
utils.set_code(to_email, code)

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save