Compare commits

...

No commits in common. 'main' and 'zh_branch' have entirely different histories.

162
.gitignore vendored

@ -1,162 +0,0 @@
# ---> Python
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/#use-with-ide
.pdm.toml
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/

@ -1,9 +0,0 @@
MIT License
Copyright (c) <year> <copyright holders>
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

@ -1,2 +0,0 @@
# DjangoBlog-Maintenance-Analysis

@ -0,0 +1,5 @@
# 默认忽略的文件
/shelf/
/workspace.xml
# 基于编辑器的 HTTP 客户端请求
/httpRequests/

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

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

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

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="jdk" jdkName="Python 3.12 (Program Files)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
<component name="PyDocumentationSettings">
<option name="format" value="PLAIN" />
<option name="myDocStringFormat" value="Plain" />
</component>
</module>

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

@ -0,0 +1,2 @@
#zh:
#codingutf-8

@ -0,0 +1,66 @@
#zh:
#codingutf-8
from django import forms
from django.contrib.auth.admin import UserAdmin
from django.contrib.auth.forms import UserChangeForm
from django.contrib.auth.forms import UsernameField
from django.utils.translation import gettext_lazy as _
# 注册模型的地方
from .models import BlogUser
class BlogUserCreationForm(forms.ModelForm):
"""自定义用户创建表单"""
password1 = forms.CharField(label=_('password'), widget=forms.PasswordInput) # 密码字段
password2 = forms.CharField(label=_('Enter password again'), widget=forms.PasswordInput) # 确认密码字段
class Meta:
model = BlogUser # 指定关联的模型
fields = ('email',) # 表单中包含的字段只包含email
def clean_password2(self):
"""验证两次输入的密码是否一致"""
password1 = self.cleaned_data.get("password1") # 获取第一次输入的密码
password2 = self.cleaned_data.get("password2") # 获取第二次输入的密码
if password1 and password2 and password1 != password2: # 如果两次密码不一致
raise forms.ValidationError(_("passwords do not match")) # 抛出验证错误
return password2 # 返回确认的密码
def save(self, commit=True):
"""保存用户,对密码进行哈希处理"""
user = super().save(commit=False) # 先不提交到数据库
user.set_password(self.cleaned_data["password1"]) # 设置哈希后的密码
if commit: # 如果需要提交
user.source = 'adminsite' # 设置用户来源为管理员站点
user.save() # 保存用户到数据库
return user # 返回用户对象
class BlogUserChangeForm(UserChangeForm):
"""自定义用户信息修改表单"""
class Meta:
model = BlogUser # 指定关联的模型
fields = '__all__' # 包含所有字段
field_classes = {'username': UsernameField} # 指定用户名字段类型
def __init__(self, *args, **kwargs):
"""初始化表单"""
super().__init__(*args, **kwargs) # 调用父类初始化方法
class BlogUserAdmin(UserAdmin):
"""自定义用户管理后台配置"""
form = BlogUserChangeForm # 使用自定义的用户修改表单
add_form = BlogUserCreationForm # 使用自定义的用户创建表单
list_display = (
'id', # 显示ID
'nickname', # 显示昵称
'username', # 显示用户名
'email', # 显示邮箱
'last_login', # 显示最后登录时间
'date_joined', # 显示注册日期
'source' # 显示用户来源
)
list_display_links = ('id', 'username') # 设置可点击的字段链接
ordering = ('-id',) # 按ID倒序排列

@ -0,0 +1,18 @@
#zh:
#codingutf-8
from django.apps import AppConfig # 导入Django应用配置基类
class AccountsConfig(AppConfig):
"""账户应用的配置类"""
# 指定应用的Python路径Django 3.x及以下版本使用
# 在Django 4.x中name字段被替换为使用应用标签
name = 'accounts'
# 在Django 4.x中可以添加以下字段
# default_auto_field = 'django.db.models.BigAutoField' # 默认主键类型
# verbose_name = '用户账户' # 人类可读的应用名称(中文)
# def ready(self):
# # 导入信号处理器等初始化代码
# import accounts.signals

@ -0,0 +1,141 @@
#zh:
#codingutf-8
from django import forms
from django.contrib.auth import get_user_model, password_validation
from django.contrib.auth.forms import AuthenticationForm, UserCreationForm
from django.core.exceptions import ValidationError
from django.forms import widgets
from django.utils.translation import gettext_lazy as _
from . import utils
from .models import BlogUser
class LoginForm(AuthenticationForm):
"""用户登录表单"""
def __init__(self, *args, **kwargs):
"""初始化表单设置字段的widget属性"""
super(LoginForm, self).__init__(*args, **kwargs)
# 设置用户名字段的输入框样式和占位符
self.fields['username'].widget = widgets.TextInput(
attrs={'placeholder': "username", "class": "form-control"})
# 设置密码字段的输入框样式和占位符
self.fields['password'].widget = widgets.PasswordInput(
attrs={'placeholder': "password", "class": "form-control"})
class RegisterForm(UserCreationForm):
"""用户注册表单"""
def __init__(self, *args, **kwargs):
"""初始化表单设置所有字段的widget属性"""
super(RegisterForm, self).__init__(*args, **kwargs)
# 设置用户名字段的输入框样式和占位符
self.fields['username'].widget = widgets.TextInput(
attrs={'placeholder': "username", "class": "form-control"})
# 设置邮箱字段的输入框样式和占位符
self.fields['email'].widget = widgets.EmailInput(
attrs={'placeholder': "email", "class": "form-control"})
# 设置密码字段的输入框样式和占位符
self.fields['password1'].widget = widgets.PasswordInput(
attrs={'placeholder': "password", "class": "form-control"})
# 设置确认密码字段的输入框样式和占位符
self.fields['password2'].widget = widgets.PasswordInput(
attrs={'placeholder': "repeat password", "class": "form-control"})
def clean_email(self):
"""验证邮箱是否已存在"""
email = self.cleaned_data['email']
# 检查邮箱是否已经被注册
if get_user_model().objects.filter(email=email).exists():
raise ValidationError(_("email already exists"))
return email
class Meta:
"""表单元数据配置"""
model = get_user_model() # 获取当前用户模型
fields = ("username", "email") # 表单包含的字段
class ForgetPasswordForm(forms.Form):
"""忘记密码重置表单"""
new_password1 = forms.CharField(
label=_("New password"), # 新密码标签
widget=forms.PasswordInput( # 密码输入框
attrs={
"class": "form-control", # CSS类名
'placeholder': _("New password") # 占位符文本
}
),
)
new_password2 = forms.CharField(
label="确认密码", # 确认密码标签
widget=forms.PasswordInput( # 密码输入框
attrs={
"class": "form-control", # CSS类名
'placeholder': _("Confirm password") # 占位符文本
}
),
)
email = forms.EmailField(
label='邮箱', # 邮箱标签
widget=forms.TextInput( # 文本输入框
attrs={
'class': 'form-control', # CSS类名
'placeholder': _("Email") # 占位符文本
}
),
)
code = forms.CharField(
label=_('Code'), # 验证码标签
widget=forms.TextInput( # 文本输入框
attrs={
'class': 'form-control', # CSS类名
'placeholder': _("Code") # 占位符文本
}
),
)
def clean_new_password2(self):
"""验证两次输入的新密码是否一致"""
password1 = self.data.get("new_password1") # 获取第一次输入的密码
password2 = self.data.get("new_password2") # 获取第二次输入的密码
if password1 and password2 and password1 != password2: # 检查密码是否一致
raise ValidationError(_("passwords do not match")) # 密码不一致报错
password_validation.validate_password(password2) # 验证密码强度
return password2 # 返回确认的密码
def clean_email(self):
"""验证邮箱是否存在"""
user_email = self.cleaned_data.get("email") # 获取邮箱
# 检查邮箱是否在系统中注册
if not BlogUser.objects.filter(email=user_email).exists():
# todo 这里的报错提示可以判断一个邮箱是不是注册过,如果不想暴露可以修改
raise ValidationError(_("email does not exist")) # 邮箱不存在报错
return user_email # 返回邮箱
def clean_code(self):
"""验证验证码是否正确"""
code = self.cleaned_data.get("code") # 获取验证码
# 调用utils模块验证验证码
error = utils.verify(
email=self.cleaned_data.get("email"), # 传入邮箱
code=code, # 传入验证码
)
if error: # 如果有错误信息
raise ValidationError(error) # 抛出验证错误
return code # 返回验证码
class ForgetPasswordCodeForm(forms.Form):
"""获取忘记密码验证码表单"""
email = forms.EmailField(
label=_('Email'), # 邮箱标签
)

@ -0,0 +1,50 @@
#zh:
#codingutf-8
import django.contrib.auth.models # 导入Django认证系统的模型类用于用户管理
import django.contrib.auth.validators # 导入Django认证系统的验证器用于用户输入验证
from django.db import migrations, models # 从Django数据库模块导入迁移工具和模型基类
import django.utils.timezone # 导入Django的时区工具用于处理时间相关操作
class Migration(migrations.Migration): # 定义迁移类继承自Django的迁移基类用于数据库结构变更
initial = True # 标记当前迁移为初始迁移,即该应用的第一个数据库迁移文件
dependencies = [ # 定义迁移依赖关系,确保迁移执行顺序正确
('auth', '0012_alter_user_first_name_max_length'), # 依赖于auth应用的特定迁移版本
]
operations = [ # 定义当前迁移需要执行的数据库操作列表
migrations.CreateModel( # 创建新数据模型的迁移操作
name='BlogUser', # 要创建的模型名称为BlogUser
fields=[ # 定义模型包含的字段列表
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), # 自增主键字段,自动创建,作为唯一标识
('password', models.CharField(max_length=128, verbose_name='password')), # 密码字段最大长度128字符
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), # 最后登录时间字段,可为空,允许空白
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), # 是否为超级用户默认False拥有所有权限
('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')), # 用户名字段唯一最长150字符使用Unicode验证器
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')), # 名可为空最长150字符
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), # 姓可为空最长150字符
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), # 邮箱字段,自动验证格式,可为空
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), # 是否为管理员默认False决定能否登录admin后台
('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')), # 账号是否激活默认True用于软删除
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), # 注册时间,默认当前时区时间
('nickname', models.CharField(blank=True, max_length=100, verbose_name='昵称')), # 自定义昵称字段可为空最长100字符
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')), # 记录用户创建时间,默认当前时区时间
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')), # 记录用户信息最后修改时间,默认当前时区时间
('source', models.CharField(blank=True, max_length=100, verbose_name='创建来源')), # 记录用户账号的创建来源可为空最长100字符
('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')), # 与用户组的多对多关系关联auth应用的Group模型
('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')), # 与权限的多对多关系关联auth应用的Permission模型
],
options={ # 模型的元数据配置
'verbose_name': '用户', # 模型的单数显示名称
'verbose_name_plural': '用户', # 模型的复数显示名称
'ordering': ['-id'], # 默认排序方式按id降序排列最新用户在前
'get_latest_by': 'id', # 指定通过id字段获取最新记录
},
managers=[ # 模型的管理器配置
('objects', django.contrib.auth.models.UserManager()), # 使用Django内置的UserManager作为模型管理器提供用户管理功能
],
),
]

@ -0,0 +1,46 @@
#zh:
#codingutf-8
from django.db import migrations, models # 导入Django数据库迁移工具和模型字段类
import django.utils.timezone # 导入Django时区工具用于处理时间相关操作
class Migration(migrations.Migration): # 定义迁移类,处理数据库结构变更
dependencies = [ # 定义当前迁移依赖的其他迁移文件
('accounts', '0001_initial'), # 依赖于accounts应用的0001_initial迁移
]
operations = [ # 定义当前迁移需要执行的数据库操作列表
migrations.AlterModelOptions( # 修改模型的元选项配置
name='bloguser', # 要修改的模型名称为BlogUser
options={'get_latest_by': 'id', 'ordering': ['-id'], 'verbose_name': 'user', 'verbose_name_plural': 'user'}, # 更新元选项,将显示名称改为英文
),
migrations.RemoveField( # 移除模型中的字段
model_name='bloguser', # 目标模型为BlogUser
name='created_time', # 要移除的字段名为created_time
),
migrations.RemoveField( # 移除模型中的另一个字段
model_name='bloguser', # 目标模型为BlogUser
name='last_mod_time', # 要移除的字段名为last_mod_time
),
migrations.AddField( # 向模型添加新字段
model_name='bloguser', # 目标模型为BlogUser
name='creation_time', # 新字段名称为creation_time
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'), # 字段类型为DateTimeField默认值为当前时间显示名为"creation time"
),
migrations.AddField( # 向模型添加另一个新字段
model_name='bloguser', # 目标模型为BlogUser
name='last_modify_time', # 新字段名称为last_modify_time
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='last modify time'), # 字段类型为DateTimeField默认值为当前时间显示名为"last modify time"
),
migrations.AlterField( # 修改模型中已有字段的配置
model_name='bloguser', # 目标模型为BlogUser
name='nickname', # 要修改的字段名为nickname
field=models.CharField(blank=True, max_length=100, verbose_name='nick name'), # 更新字段显示名为"nick name",其他属性保持不变
),
migrations.AlterField( # 修改模型中另一个已有字段的配置
model_name='bloguser', # 目标模型为BlogUser
name='source', # 要修改的字段名为source
field=models.CharField(blank=True, max_length=100, verbose_name='create source'), # 更新字段显示名为"create source",其他属性保持不变
),
]

@ -0,0 +1,2 @@
#zh:
#codingutf-8

@ -0,0 +1,50 @@
#zh:
#codingutf-8
from django.contrib.auth.models import AbstractUser # 导入Django内置的抽象用户基类
from django.db import models # 导入Django的模型模块
from django.urls import reverse # 用于生成URL反向解析
from django.utils.timezone import now # 获取当前时间
from django.utils.translation import gettext_lazy as _ # 国际化翻译
from django.utils import get_current_site # 获取当前站点信息
# 在此处创建模型
class BlogUser(AbstractUser):
"""自定义博客用户模型继承自Django的AbstractUser"""
# 昵称字段最大长度100字符允许为空
nickname = models.CharField(_('nick name'), max_length=100, blank=True)
# 创建时间字段,默认值为当前时间
creation_time = models.DateTimeField(_('creation time'), default=now)
# 最后修改时间字段,默认值为当前时间
last_modify_time = models.DateTimeField(_('last modify time'), default=now)
# 用户来源字段,记录用户创建来源(如网站注册、管理员创建等)
source = models.CharField(_('create source'), max_length=100, blank=True)
def get_absolute_url(self):
"""获取用户的绝对URL用于生成作者详情页链接"""
return reverse(
'blog:author_detail', kwargs={ # 反向解析URL
'author_name': self.username}) # 使用用户名作为URL参数
def __str__(self):
"""对象的字符串表示,返回邮箱地址"""
return self.email
def get_full_url(self):
"""获取完整的用户URL包含域名"""
site = get_current_site().domain # 获取当前站点的域名
url = "https://{site}{path}".format(site=site, # 格式化完整URL
path=self.get_absolute_url())
return url
class Meta:
"""模型的元数据配置"""
ordering = ['-id'] # 默认按ID降序排列
verbose_name = _('user') # 单数显示名称
verbose_name_plural = verbose_name # 复数显示名称(与单数相同)
get_latest_by = 'id' # 指定获取最新记录的字段

@ -0,0 +1,2 @@
#zh:
#codingutf-8

@ -0,0 +1,245 @@
#zh:
#codingutf-8
from django.test import Client, RequestFactory, TestCase
from django.urls import reverse
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from accounts.models import BlogUser
from blog.models import Article, Category
from djangoblog.utils import *
from . import utils
# 在此处创建测试
class AccountTest(TestCase):
"""账户功能测试类"""
def setUp(self):
"""测试初始化方法,在每个测试方法执行前运行"""
self.client = Client() # 创建测试客户端
self.factory = RequestFactory() # 创建请求工厂
# 创建测试用户
self.blog_user = BlogUser.objects.create_user(
username="test",
email="admin@admin.com",
password="12345678"
)
self.new_test = "xxx123--=" # 新密码用于测试
def test_validate_account(self):
"""测试账户验证功能"""
site = get_current_site().domain # 获取当前站点域名
# 创建超级用户
user = BlogUser.objects.create_superuser(
email="liangliangyy1@gmail.com",
username="liangliangyy1",
password="qwer!@#$ggg")
testuser = BlogUser.objects.get(username='liangliangyy1')
# 测试登录功能
loginresult = self.client.login(
username='liangliangyy1',
password='qwer!@#$ggg')
self.assertEqual(loginresult, True) # 断言登录成功
# 测试访问管理员页面
response = self.client.get('/admin/')
self.assertEqual(response.status_code, 200) # 断言可以访问管理员页面
# 创建测试分类
category = Category()
category.name = "categoryaaa"
category.creation_time = timezone.now()
category.last_modify_time = timezone.now()
category.save()
# 创建测试文章
article = Article()
article.title = "nicetitleaaa"
article.body = "nicecontentaaa"
article.author = user
article.category = category
article.type = 'a' # 文章类型
article.status = 'p' # 发布状态
article.save()
# 测试访问文章管理页面
response = self.client.get(article.get_admin_url())
self.assertEqual(response.status_code, 200) # 断言可以访问文章管理页面
def test_validate_register(self):
"""测试用户注册功能"""
# 验证注册前用户不存在
self.assertEquals(
0, len(
BlogUser.objects.filter(
email='user123@user.com')))
# 发送注册请求
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',
})
# 验证注册后用户存在
self.assertEquals(
1, len(
BlogUser.objects.filter(
email='user123@user.com')))
# 获取新注册的用户
user = BlogUser.objects.filter(email='user123@user.com')[0]
# 生成验证签名
sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
path = reverse('accounts:result')
url = '{path}?type=validation&id={id}&sign={sign}'.format(
path=path, id=user.id, sign=sign)
# 测试验证页面访问
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
# 测试用户登录
self.client.login(username='user1233', password='password123!q@wE#R$T')
user = BlogUser.objects.filter(email='user123@user.com')[0]
# 提升用户权限
user.is_superuser = True
user.is_staff = True
user.save()
delete_sidebar_cache() # 清除侧边栏缓存
# 创建测试分类
category = Category()
category.name = "categoryaaa"
category.creation_time = timezone.now()
category.last_modify_time = timezone.now()
category.save()
# 创建测试文章
article = Article()
article.category = category
article.title = "nicetitle333"
article.body = "nicecontentttt"
article.author = user
article.type = 'a'
article.status = 'p'
article.save()
# 测试文章管理页面访问
response = self.client.get(article.get_admin_url())
self.assertEqual(response.status_code, 200)
# 测试用户登出
response = self.client.get(reverse('account:logout'))
self.assertIn(response.status_code, [301, 302, 200]) # 登出后重定向
# 测试登出后访问文章管理页面(应该被重定向)
response = self.client.get(article.get_admin_url())
self.assertIn(response.status_code, [301, 302, 200])
# 测试错误密码登录
response = self.client.post(reverse('account:login'), {
'username': 'user1233',
'password': 'password123' # 错误密码
})
self.assertIn(response.status_code, [301, 302, 200])
# 测试错误登录后访问文章管理页面
response = self.client.get(article.get_admin_url())
self.assertIn(response.status_code, [301, 302, 200])
def test_verify_email_code(self):
"""测试邮箱验证码功能"""
to_email = "admin@admin.com"
code = generate_code() # 生成验证码
utils.set_code(to_email, code) # 设置验证码
utils.send_verify_email(to_email, code) # 发送验证邮件
# 测试正确验证码验证
err = utils.verify("admin@admin.com", code)
self.assertEqual(err, None) # 断言验证成功,无错误
# 测试错误邮箱验证
err = utils.verify("admin@123.com", code)
self.assertEqual(type(err), str) # 断言返回错误信息
def test_forget_password_email_code_success(self):
"""测试成功发送忘记密码验证码"""
resp = self.client.post(
path=reverse("account:forget_password_code"),
data=dict(email="admin@admin.com")
)
self.assertEqual(resp.status_code, 200) # 断言请求成功
self.assertEqual(resp.content.decode("utf-8"), "ok") # 断言返回成功信息
def test_forget_password_email_code_fail(self):
"""测试发送忘记密码验证码失败情况"""
# 测试空邮箱
resp = self.client.post(
path=reverse("account:forget_password_code"),
data=dict()
)
self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱")
# 测试无效邮箱格式
resp = self.client.post(
path=reverse("account:forget_password_code"),
data=dict(email="admin@com")
)
self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱")
def test_forget_password_email_success(self):
"""测试成功重置密码"""
code = generate_code() # 生成验证码
utils.set_code(self.blog_user.email, code) # 设置验证码
data = dict(
new_password1=self.new_test, # 新密码
new_password2=self.new_test, # 确认密码
email=self.blog_user.email, # 用户邮箱
code=code, # 验证码
)
resp = self.client.post(
path=reverse("account:forget_password"),
data=data
)
self.assertEqual(resp.status_code, 302) # 断言重定向(成功)
# 验证用户密码是否修改成功
blog_user = BlogUser.objects.filter(
email=self.blog_user.email,
).first() # type: BlogUser
self.assertNotEqual(blog_user, None) # 断言用户存在
self.assertEqual(blog_user.check_password(data["new_password1"]), True) # 断言密码修改成功
def test_forget_password_email_not_user(self):
"""测试不存在的用户重置密码"""
data = dict(
new_password1=self.new_test,
new_password2=self.new_test,
email="123@123.com", # 不存在的邮箱
code="123456",
)
resp = self.client.post(
path=reverse("account:forget_password"),
data=data
)
self.assertEqual(resp.status_code, 200) # 断言停留在当前页面(失败)
def test_forget_password_email_code_error(self):
"""测试验证码错误的重置密码"""
code = generate_code()
utils.set_code(self.blog_user.email, code)
data = dict(
new_password1=self.new_test,
new_password2=self.new_test,
email=self.blog_user.email,
code="111111", # 错误的验证码
)
resp = self.client.post(
path=reverse("account:forget_password"),
data=data
)
self.assertEqual(resp.status_code, 200) # 断言停留在当前页面(失败)

@ -0,0 +1,42 @@
#zh:
#codingutf-8
from django.urls import path # 导入路径路由
from django.urls import re_path # 导入正则表达式路由
from . import views # 导入当前应用的视图模块
from .forms import LoginForm # 导入自定义登录表单
app_name = "accounts" # 定义应用命名空间用于URL反向解析
urlpatterns = [
# 用户登录路由
re_path(r'^login/$', # 匹配以/login/结尾的URL
views.LoginView.as_view(success_url='/'), # 使用类视图,登录成功后跳转到首页
name='login', # URL名称用于反向解析
kwargs={'authentication_form': LoginForm}), # 传递自定义登录表单
# 用户注册路由
re_path(r'^register/$', # 匹配以/register/结尾的URL
views.RegisterView.as_view(success_url="/"), # 使用类视图,注册成功后跳转到首页
name='register'), # URL名称用于反向解析
# 用户登出路由
re_path(r'^logout/$', # 匹配以/logout/结尾的URL
views.LogoutView.as_view(), # 使用类视图处理登出
name='logout'), # URL名称用于反向解析
# 账户操作结果页面路由
path(r'account/result.html', # 精确匹配/account/result.html路径
views.account_result, # 使用函数视图
name='result'), # URL名称用于反向解析
# 忘记密码页面路由(表单提交)
re_path(r'^forget_password/$', # 匹配以/forget_password/结尾的URL
views.ForgetPasswordView.as_view(), # 使用类视图处理忘记密码逻辑
name='forget_password'), # URL名称用于反向解析
# 获取忘记密码验证码路由
re_path(r'^forget_password_code/$', # 匹配以/forget_password_code/结尾的URL
views.ForgetPasswordEmailCode.as_view(), # 使用类视图发送验证码
name='forget_password_code'), # URL名称用于反向解析
]

@ -0,0 +1,59 @@
#zh:
#codingutf-8
from django.contrib.auth import get_user_model # 导入获取用户模型的方法
from django.contrib.auth.backends import ModelBackend # 导入Django认证后端基类
class EmailOrUsernameModelBackend(ModelBackend):
"""
自定义认证后端允许使用用户名或邮箱登录
扩展了Django的默认认证系统支持更灵活的登录方式
"""
def authenticate(self, request, username=None, password=None, **kwargs):
"""
用户认证方法
重写父类方法支持用户名和邮箱两种登录方式
Args:
request: HTTP请求对象
username: 用户输入的用户名或邮箱
password: 用户输入的密码
**kwargs: 其他参数
Returns:
User: 认证成功的用户对象
None: 认证失败
"""
# 判断输入的是邮箱还是用户名
if '@' in username:
kwargs = {'email': username} # 如果包含@符号,按邮箱处理
else:
kwargs = {'username': username} # 否则按用户名处理
try:
# 根据用户名或邮箱查找用户
user = get_user_model().objects.get(**kwargs)
# 验证密码是否正确
if user.check_password(password):
return user # 认证成功,返回用户对象
except get_user_model().DoesNotExist:
# 用户不存在返回None表示认证失败
return None
def get_user(self, user_id):
"""
根据用户ID获取用户对象
用于会话管理保持用户登录状态
Args:
user_id: 用户ID
Returns:
User: 用户对象
None: 用户不存在
"""
try:
return get_user_model().objects.get(pk=user_id) # 根据主键查找用户
except get_user_model().DoesNotExist:
return None # 用户不存在

@ -0,0 +1,76 @@
#zh:
#codingutf-8
import typing # 导入类型提示模块
from datetime import timedelta # 导入时间间隔模块
from django.core.cache import cache # 导入Django缓存框架
from django.utils.translation import gettext # 导入翻译函数
from django.utils.translation import gettext_lazy as _ # 导入惰性翻译
from djangoblog.utils import send_email # 导入自定义邮件发送工具
# 定义验证码的有效期5分钟
_code_ttl = timedelta(minutes=5)
def send_verify_email(to_mail: str, code: str, subject: str = _("Verify Email")):
"""
发送验证邮件用于密码重置等场景
Args:
to_mail: 接收邮箱地址
code: 验证码内容
subject: 邮件主题默认为"Verify Email"
"""
# 构建邮件HTML内容包含验证码信息
html_content = _(
"You are resetting the password, the verification code is%(code)s, valid within 5 minutes, please keep it "
"properly") % {'code': code}
# 调用邮件发送函数发送邮件
send_email([to_mail], subject, html_content)
def verify(email: str, code: str) -> typing.Optional[str]:
"""
验证邮箱验证码是否正确
Args:
email: 邮箱地址
code: 用户输入的验证码
Returns:
str: 如果验证失败返回错误信息验证成功返回None
Note:
这里的错误处理不太合理应该采用raise抛出异常
否则调用方也需要对error进行处理
"""
cache_code = get_code(email) # 从缓存中获取该邮箱对应的验证码
if cache_code != code: # 比较缓存中的验证码和用户输入的验证码
return gettext("Verification code error") # 验证码错误,返回错误信息
# 验证成功返回None
def set_code(email: str, code: str):
"""
将验证码存储到缓存中
Args:
email: 邮箱地址作为缓存的key
code: 验证码作为缓存的value
"""
# 使用邮箱作为key验证码作为value设置过期时间为5分钟
cache.set(email, code, _code_ttl.seconds)
def get_code(email: str) -> typing.Optional[str]:
"""
从缓存中获取验证码
Args:
email: 邮箱地址缓存的key
Returns:
str: 如果存在返回验证码不存在返回None
"""
return cache.get(email) # 从缓存中获取指定邮箱的验证码

@ -0,0 +1,249 @@
#zh:
#codingutf-8
import logging
from django.utils.translation import gettext_lazy as _
from django.conf import settings
from django.contrib import auth
from django.contrib.auth import REDIRECT_FIELD_NAME
from django.contrib.auth import get_user_model
from django.contrib.auth import logout
from django.contrib.auth.forms import AuthenticationForm
from django.contrib.auth.hashers import make_password
from django.http import HttpResponseRedirect, HttpResponseForbidden
from django.http.request import HttpRequest
from django.http.response import HttpResponse
from django.shortcuts import get_object_or_404
from django.shortcuts import render
from django.urls import reverse
from django.utils.decorators import method_decorator
from django.utils.http import url_has_allowed_host_and_scheme
from django.views import View
from django.views.decorators.cache import never_cache
from django.views.decorators.csrf import csrf_protect
from django.views.decorators.debug import sensitive_post_parameters
from django.views.generic import FormView, RedirectView
from djangoblog.utils import send_email, get_sha256, get_current_site, generate_code, delete_sidebar_cache
from . import utils
from .forms import RegisterForm, LoginForm, ForgetPasswordForm, ForgetPasswordCodeForm
from .models import BlogUser
# 获取当前模块的日志记录器
logger = logging.getLogger(__name__)
# 在此处创建视图
class RegisterView(FormView):
"""用户注册视图"""
form_class = RegisterForm # 使用自定义注册表单
template_name = 'account/registration_form.html' # 注册模板路径
@method_decorator(csrf_protect) # CSRF保护装饰器
def dispatch(self, *args, **kwargs):
"""请求分发方法"""
return super(RegisterView, self).dispatch(*args, **kwargs)
def form_valid(self, form):
"""表单验证通过后的处理"""
if form.is_valid():
# 保存用户但不提交到数据库
user = form.save(False)
user.is_active = False # 设置用户为未激活状态
user.source = 'Register' # 记录用户来源
user.save(True) # 保存用户到数据库
# 获取当前站点信息
site = get_current_site().domain
# 生成邮箱验证签名
sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
# 调试模式下使用本地地址
if settings.DEBUG:
site = '127.0.0.1:8000'
# 构建验证URL
path = reverse('account:result')
url = "http://{site}{path}?type=validation&id={id}&sign={sign}".format(
site=site, path=path, id=user.id, sign=sign)
# 构建邮件内容
content = """
<p>请点击下面链接验证您的邮箱</p>
<a href="{url}" rel="bookmark">{url}</a>
再次感谢您
<br />
如果上面链接无法打开请将此链接复制至浏览器
{url}
""".format(url=url)
# 发送验证邮件
send_email(
emailto=[user.email],
title='验证您的电子邮箱',
content=content)
# 重定向到结果页面
url = reverse('accounts:result') + '?type=register&id=' + str(user.id)
return HttpResponseRedirect(url)
else:
# 表单无效,重新渲染表单页
return self.render_to_response({'form': form})
class LogoutView(RedirectView):
"""用户登出视图"""
url = '/login/' # 登出后重定向的URL
@method_decorator(never_cache) # 禁止缓存装饰器
def dispatch(self, request, *args, **kwargs):
"""请求分发方法"""
return super(LogoutView, self).dispatch(request, *args, **kwargs)
def get(self, request, *args, **kwargs):
"""处理GET请求"""
logout(request) # 执行登出操作
delete_sidebar_cache() # 删除侧边栏缓存
return super(LogoutView, self).get(request, *args, **kwargs)
class LoginView(FormView):
"""用户登录视图"""
form_class = LoginForm # 使用自定义登录表单
template_name = 'account/login.html' # 登录模板路径
success_url = '/' # 登录成功默认重定向URL
redirect_field_name = REDIRECT_FIELD_NAME # 重定向字段名
login_ttl = 2626560 # 会话有效期:一个月(以秒为单位)
# 方法装饰器保护敏感数据、CSRF防护、禁止缓存
@method_decorator(sensitive_post_parameters('password'))
@method_decorator(csrf_protect)
@method_decorator(never_cache)
def dispatch(self, request, *args, **kwargs):
"""请求分发方法"""
return super(LoginView, self).dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
"""获取模板上下文数据"""
# 获取重定向URL
redirect_to = self.request.GET.get(self.redirect_field_name)
if redirect_to is None:
redirect_to = '/' # 默认重定向到首页
kwargs['redirect_to'] = redirect_to
return super(LoginView, self).get_context_data(**kwargs)
def form_valid(self, form):
"""表单验证通过后的处理"""
# 使用Django内置的认证表单
form = AuthenticationForm(data=self.request.POST, request=self.request)
if form.is_valid():
# 清除侧边栏缓存
delete_sidebar_cache()
logger.info(self.redirect_field_name)
# 执行登录操作
auth.login(self.request, form.get_user())
# 处理"记住我"功能
if self.request.POST.get("remember"):
self.request.session.set_expiry(self.login_ttl) # 设置会话有效期
return super(LoginView, self).form_valid(form)
else:
# 表单无效,重新渲染表单页
return self.render_to_response({'form': form})
def get_success_url(self):
"""获取登录成功后的重定向URL"""
redirect_to = self.request.POST.get(self.redirect_field_name)
# 验证URL安全性
if not url_has_allowed_host_and_scheme(
url=redirect_to, allowed_hosts=[self.request.get_host()]):
redirect_to = self.success_url # 不安全的URL使用默认URL
return redirect_to
def account_result(request):
"""账户操作结果页面视图函数"""
type = request.GET.get('type') # 操作类型
id = request.GET.get('id') # 用户ID
# 获取用户对象不存在则返回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':
# 注册成功页面内容
content = '''
恭喜您注册成功一封验证邮件已经发送到您的邮箱请验证您的邮箱后登录本站
'''
title = '注册成功'
else:
# 邮箱验证处理
c_sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
sign = request.GET.get('sign')
# 验证签名
if sign != c_sign:
return HttpResponseForbidden() # 签名不匹配,拒绝访问
# 激活用户
user.is_active = True
user.save()
content = '''
恭喜您已经成功的完成邮箱验证您现在可以使用您的账号来登录本站
'''
title = '验证成功'
# 渲染结果页面
return render(request, 'account/result.html', {
'title': title,
'content': content
})
else:
# 无效类型,重定向到首页
return HttpResponseRedirect('/')
class ForgetPasswordView(FormView):
"""忘记密码重置视图"""
form_class = ForgetPasswordForm # 使用忘记密码表单
template_name = 'account/forget_password.html' # 模板路径
def form_valid(self, form):
"""表单验证通过后的处理"""
if form.is_valid():
# 获取用户并重置密码
blog_user = BlogUser.objects.filter(email=form.cleaned_data.get("email")).get()
# 对密码进行哈希处理
blog_user.password = make_password(form.cleaned_data["new_password2"])
blog_user.save() # 保存用户
return HttpResponseRedirect('/login/') # 重定向到登录页
else:
# 表单无效,重新渲染表单页
return self.render_to_response({'form': form})
class ForgetPasswordEmailCode(View):
"""忘记密码验证码发送视图"""
def post(self, request: HttpRequest):
"""处理POST请求发送验证码"""
form = ForgetPasswordCodeForm(request.POST)
if not form.is_valid():
return HttpResponse("错误的邮箱") # 表单验证失败
to_email = form.cleaned_data["email"]
# 生成并发送验证码
code = generate_code()
utils.send_verify_email(to_email, code)
utils.set_code(to_email, code)
return HttpResponse("ok") # 返回成功响应

@ -0,0 +1,65 @@
from django.contrib import admin
from django.urls import reverse
from django.utils.html import format_html # 用于安全地生成HTML内容
from django.utils.translation import gettext_lazy as _ # 用于国际化翻译
# 自定义批量操作:禁用评论状态
def disable_commentstatus(modeladmin, request, queryset):
queryset.update(is_enable=False) # 将选中的评论记录is_enable字段设为False
# 自定义批量操作:启用评论状态
def enable_commentstatus(modeladmin, request, queryset):
queryset.update(is_enable=True) # 将选中的评论记录is_enable字段设为True
# 为批量操作设置显示名称(支持国际化)
disable_commentstatus.short_description = _('Disable comments')
enable_commentstatus.short_description = _('Enable comments')
class CommentAdmin(admin.ModelAdmin):
list_per_page = 20 # 每页显示20条记录
list_display = (
'id',
'body', # 评论内容
'link_to_userinfo', # 自定义字段:链接到用户信息
'link_to_article', # 自定义字段:链接到文章
'is_enable', # 是否启用
'creation_time' # 创建时间
)
# 列表页中可点击跳转编辑页的字段
list_display_links = ('id', 'body', 'is_enable')
# 可筛选的字段(右侧过滤器)
list_filter = ('is_enable',)
# 编辑页排除的字段(不允许编辑,如自动生成的时间)
exclude = ('creation_time', 'last_modify_time')
# 注册自定义批量操作
actions = [disable_commentstatus, enable_commentstatus]
# 自定义列表字段:生成用户信息的编辑链接
def link_to_userinfo(self, obj):
# 获取用户模型的app标签和模型名称
info = (obj.author._meta.app_label, obj.author._meta.model_name)
# 生成用户编辑页的URL
link = reverse('admin:%s_%s_change' % info, args=(obj.author.id,))
# 返回带链接的HTML优先显示昵称无昵称则显示邮箱
return format_html(
u'<a href="%s">%s</a>' %
(link, obj.author.nickname if obj.author.nickname else obj.author.email))
# 自定义列表字段:生成文章的编辑链接
def link_to_article(self, obj):
# 获取文章模型的app标签和模型名称
info = (obj.article._meta.app_label, obj.article._meta.model_name)
# 生成文章编辑页的URL
link = reverse('admin:%s_%s_change' % info, args=(obj.article.id,))
# 返回带链接的HTML显示文章标题
return format_html(
u'<a href="%s">%s</a>' % (link, obj.article.title))
# 自定义字段的显示名称(支持国际化)
link_to_userinfo.short_description = _('User')
link_to_article.short_description = _('Article')

@ -0,0 +1,7 @@
from django.apps import AppConfig
class CommentsConfig(AppConfig):
# 配置应用的名称,对应项目中该应用的目录名)
# Django通过这个名称识别和管理该应用
name = 'comments'

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

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

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

@ -0,0 +1,20 @@
{% load i18n %}
{% if article_tags_list %} <!-- 判断文章标签列表是否存在,存在则渲染标签面板 -->
<div class="panel panel-default"> <!-- 标签面板容器,使用默认样式的面板组件 -->
<div class="panel-heading"> <!-- 面板头部区域,显示标题 -->
{% trans 'tags' %} <!-- 翻译“tags”为对应语言如中文“标签”作为面板标题 -->
</div>
<div class="panel-body"> <!-- 面板内容区域,用于放置标签列表 -->
{% for url,count,tag,color in article_tags_list %} <!-- 循环遍历标签列表,获取每个标签的链接、数量、标签对象、颜色 -->
<!-- 标签链接使用label组件样式颜色由循环变量color控制 -->
<a class="label label-{{ color }}" style="display: inline-block;" href="{{ url }}"
title="{{ tag.name }}"> <!-- 鼠标悬停时显示标签名称 -->
{{ tag.name }} <!-- 显示标签名称 -->
<span class="badge">{{ count }}</span> <!-- 显示该标签下的文章数量用badge组件样式包裹 -->
</a>
{% endfor %} <!-- 标签循环结束 -->
</div> <!-- 面板内容区域结束 -->
</div> <!-- 标签面板容器结束 -->
{% endif %} <!-- 标签列表存在判断结束 -->

@ -0,0 +1,25 @@
<!-- 面包屑导航列表使用Schema.org规范标记提升SEO和结构化数据识别 -->
<ul itemscope itemtype="https://schema.org/BreadcrumbList" class="breadcrumb">
{% for name,url in names %} <!-- 循环遍历面包屑导航的每一级(除最后一级) -->
<!-- 每一级导航项符合Schema.org的ListItem类型 -->
<li itemprop="itemListElement" itemscope
itemtype="https://schema.org/ListItem">
<!-- 导航项链接itemprop="item"标记链接地址 -->
<a href="{{ url }}" itemprop="item" >
<span itemprop="name">{{ name }}</span></a> <!-- 导航项名称itemprop="name"标记 -->
<!-- 标记当前导航项在列表中的位置从1开始 -->
<meta itemprop="position" content="{{ forloop.counter }}"/>
</li>
{% endfor %} <!-- 导航项循环结束 -->
<!-- 面包屑最后一级当前页面添加active类表示激活状态 -->
<li class="active" itemprop="itemListElement" itemscope
itemtype="https://schema.org/ListItem">
<span itemprop="name">{{ title }}</span> <!-- 当前页面名称,无链接 -->
<!-- 标记最后一级在列表中的位置由count变量指定 -->
<meta itemprop="position" content="{{ count }}"/>
</li>
</ul> <!-- 面包屑导航列表结束 -->

@ -0,0 +1,15 @@
from django import forms
from django.forms import ModelForm
from .models import Comment
class CommentForm(ModelForm): # 定义评论表单类继承自ModelForm
# 添加父评论ID字段用于实现评论回复功能
# 使用HiddenInput控件隐藏显示且非必填顶级评论无需父ID
parent_comment_id = forms.IntegerField(
widget=forms.HiddenInput, required=False)
class Meta: # Meta类用于配置表单与模型的关联信息
model = Comment # 指定表单对应的模型为Comment
fields = ['body'] # 表单需要包含的模型字段这里只包含评论内容body

@ -0,0 +1,38 @@
from django.conf import settings
from django.db import models
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from blog.models import Article
# 评论模型,存储用户对文章的评论及评论间的嵌套关系
class Comment(models.Model):
body = models.TextField('正文', max_length=300) # 评论内容限制最大长度300字符
creation_time = models.DateTimeField(_('creation time'), default=now) # 评论创建时间,默认当前时间
last_modify_time = models.DateTimeField(_('last modify time'), default=now) # 评论最后修改时间,默认当前时间
author = models.ForeignKey(
settings.AUTH_USER_MODEL, # 关联Django内置用户模型便于扩展用户系统
verbose_name=_('author'),
on_delete=models.CASCADE) # 级联删除:用户删除时,其评论也会被删除
article = models.ForeignKey(
Article,
verbose_name=_('article'),
on_delete=models.CASCADE) # 级联删除:文章删除时,其下所有评论也会被删除
parent_comment = models.ForeignKey(
'self', # 自关联,实现评论嵌套回复功能
verbose_name=_('parent comment'),
blank=True,
null=True, # 允许为空,表示该评论是顶级评论(不是回复)
on_delete=models.CASCADE) # 级联删除:父评论删除时,其所有子评论也会被删除
is_enable = models.BooleanField(_('enable'),
default=False, blank=False, null=False) # 评论是否启用(可用于审核功能)
class Meta:
ordering = ['-id'] # 默认按ID降序排列最新评论显示在前面
verbose_name = _('comment')
verbose_name_plural = verbose_name
get_latest_by = 'id' # 指定通过id字段获取最新记录
def __str__(self):
return self.body

@ -0,0 +1,11 @@
# 导入Django的admin模块用于在管理后台注册和管理数据模型
from django.contrib import admin
# Register your models here. # Django自动生成的注释提示在此处注册需要在管理后台显示的模型
# 定义一个管理配置类OwnTrackLogsAdmin继承自admin.ModelAdmin
# 这个类用于配置OwnTrackLog模型在Django管理后台的显示和操作方式
class OwnTrackLogsAdmin(admin.ModelAdmin):
# pass表示暂时不添加任何自定义配置使用默认的管理后台设置
# 后续可以在这里添加各种属性如list_display、search_fields等来自定义管理界面
pass

@ -0,0 +1,10 @@
# 导入Django的AppConfig类用于配置应用的元数据和初始化行为
from django.apps import AppConfig
# 定义一个应用配置类OwntracksConfig继承自AppConfig
# 该类用于配置名为'owntracks'的Django应用
class OwntracksConfig(AppConfig):
# name属性指定了应用的名称必须与应用的目录名一致
# 这个名称会被Django用于识别和管理该应用
name = 'owntracks'

@ -0,0 +1,47 @@
# 导入Django的models模块用于定义数据模型
from django.db import models
# 导入Django的时区工具用于处理时间相关操作
from django.utils.timezone import now
# Create your models here. # Django自动生成的注释提示在此处创建数据模型
# 定义一个名为OwnTrackLog的数据模型类继承自models.Model
# 这个模型用于存储OwnTracks一个位置追踪应用的位置日志信息
class OwnTrackLog(models.Model):
# 定义tid字段CharField表示字符串类型
# max_length=100限制最大长度为100字符
# null=False表示该字段不允许为空
# verbose_name='用户'用于在admin后台显示的字段名称
tid = models.CharField(max_length=100, null=False, verbose_name='用户')
# 定义lat字段FloatField表示浮点型用于存储纬度信息
# verbose_name='纬度'用于在admin后台显示的字段名称
lat = models.FloatField(verbose_name='纬度')
# 定义lon字段FloatField表示浮点型用于存储经度信息
# verbose_name='经度'用于在admin后台显示的字段名称
lon = models.FloatField(verbose_name='经度')
# 定义creation_time字段DateTimeField表示日期时间类型
# '创建时间'是字段的位置参数等同于verbose_name='创建时间'
# default=now设置默认值为当前时间使用Django的时区设置
creation_time = models.DateTimeField('创建时间', default=now)
# 定义对象的字符串表示方法
# 当打印该模型的实例时会返回tid字段的值
def __str__(self):
return self.tid
# Meta类用于定义模型的元数据
class Meta:
# ordering=['creation_time']指定查询该模型数据时的默认排序方式
# 按creation_time字段升序排列加负号表示降序如['-creation_time']
ordering = ['creation_time']
# verbose_name定义模型在admin后台的单数显示名称
verbose_name = "OwnTrackLogs"
# verbose_name_plural定义模型在admin后台的复数显示名称
verbose_name_plural = verbose_name
# get_latest_by='creation_time'指定使用creation_time字段来获取最新记录
# 可以通过模型管理器的latest()方法获取最新记录
get_latest_by = 'creation_time'

@ -0,0 +1,105 @@
# 导入json模块用于处理JSON数据格式
import json
# 从Django测试框架导入必要的测试工具
# Client用于模拟用户在视图上的请求
# RequestFactory用于创建请求对象
# TestCase是Django测试的基础类
from django.test import Client, RequestFactory, TestCase
# 导入账户模型BlogUser用于测试用户相关功能
from accounts.models import BlogUser
# 从当前应用导入要测试的模型OwnTrackLog
from .models import OwnTrackLog
# Create your tests here. # Django自动生成的注释提示在此处编写测试代码
# 定义测试类OwnTrackLogTest继承自TestCase
# 该类包含对OwnTrackLog模型及相关视图的测试用例
class OwnTrackLogTest(TestCase):
# setUp方法在每个测试方法执行前运行用于初始化测试环境
def setUp(self):
# 创建一个测试客户端,用于模拟用户请求
self.client = Client()
# 创建一个请求工厂,用于构建复杂的请求对象
self.factory = RequestFactory()
# 定义具体的测试方法方法名以test_开头
def test_own_track_log(self):
# 定义一个符合要求的测试数据字典包含tid、lat、lon字段
o = {
'tid': 12,
'lat': 123.123,
'lon': 134.341
}
# 使用测试客户端发送POST请求到指定URL
# 发送JSON格式的数据指定content_type为application/json
self.client.post(
'/owntracks/logtracks', # 请求的URL
json.dumps(o), # 将字典转换为JSON字符串
content_type='application/json') # 指定内容类型
# 检查数据库中OwnTrackLog记录的数量
length = len(OwnTrackLog.objects.all())
# 断言记录数为1验证第一条数据成功保存
self.assertEqual(length, 1)
# 定义一个不完整的测试数据字典缺少lon字段
o = {
'tid': 12,
'lat': 123.123
}
# 再次发送POST请求使用不完整的数据
self.client.post(
'/owntracks/logtracks',
json.dumps(o),
content_type='application/json')
# 再次检查数据库中记录的数量
length = len(OwnTrackLog.objects.all())
# 断言记录数仍为1验证不完整数据没有被保存
self.assertEqual(length, 1)
# 测试未登录状态下访问/show_maps页面
rsp = self.client.get('/owntracks/show_maps')
# 断言返回状态码为302重定向验证未登录用户被重定向
self.assertEqual(rsp.status_code, 302)
# 创建一个超级用户,用于测试登录状态下的功能
user = BlogUser.objects.create_superuser(
email="liangliangyy1@gmail.com",
username="liangliangyy1",
password="liangliangyy1")
# 使用测试客户端登录刚刚创建的用户
self.client.login(username='liangliangyy1', password='liangliangyy1')
# 手动创建并保存一条OwnTrackLog记录
s = OwnTrackLog()
s.tid = 12
s.lon = 123.234
s.lat = 34.234
s.save()
# 测试登录状态下访问/show_dates页面
rsp = self.client.get('/owntracks/show_dates')
# 断言返回状态码为200成功
self.assertEqual(rsp.status_code, 200)
# 测试登录状态下访问/show_maps页面
rsp = self.client.get('/owntracks/show_maps')
# 断言返回状态码为200成功
self.assertEqual(rsp.status_code, 200)
# 测试登录状态下访问/get_datas页面
rsp = self.client.get('/owntracks/get_datas')
# 断言返回状态码为200成功
self.assertEqual(rsp.status_code, 200)
# 测试登录状态下带日期参数访问/get_datas页面
rsp = self.client.get('/owntracks/get_datas?date=2018-02-26')
# 断言返回状态码为200成功
self.assertEqual(rsp.status_code, 200)

@ -0,0 +1,28 @@
# 导入Django的path函数用于定义URL路径
from django.urls import path
# 从当前应用导入views模块包含视图函数
from . import views
# 定义应用的命名空间为"owntracks"
# 用于在模板中引用URL时避免命名冲突格式为"app_name:url_name"
app_name = "owntracks"
# 定义URL模式列表每个path对应一个URL路径与视图函数的映射
urlpatterns = [
# 定义路径'owntracks/logtracks'映射到views.manage_owntrack_log视图函数
# name='logtracks'为该URL指定名称用于在模板和代码中引用
path('owntracks/logtracks', views.manage_owntrack_log, name='logtracks'),
# 定义路径'owntracks/show_maps'映射到views.show_maps视图函数
# name='show_maps'为该URL指定名称
path('owntracks/show_maps', views.show_maps, name='show_maps'),
# 定义路径'owntracks/get_datas'映射到views.get_datas视图函数
# name='get_datas'为该URL指定名称
path('owntracks/get_datas', views.get_datas, name='get_datas'),
# 定义路径'owntracks/show_dates'映射到views.show_log_dates视图函数
# name='show_dates'为该URL指定名称
path('owntracks/show_dates', views.show_log_dates, name='show_dates')
]

@ -0,0 +1,170 @@
# Create your views here.
# 导入必要的模块
import datetime # 处理日期时间
import itertools # 提供迭代器相关功能
import json # 处理JSON数据
import logging # 日志记录
from datetime import timezone # 时区处理
from itertools import groupby # 用于对序列进行分组
import django # Django框架核心
import requests # 发送HTTP请求
from django.contrib.auth.decorators import login_required # 登录验证装饰器
from django.http import HttpResponse # HTTP响应
from django.http import JsonResponse # JSON格式响应
from django.shortcuts import render # 渲染模板
from django.views.decorators.csrf import csrf_exempt # 禁用CSRF验证装饰器
from .models import OwnTrackLog # 导入当前应用的模型
# 配置日志记录器
logger = logging.getLogger(__name__)
# 禁用CSRF验证的视图函数用于处理OwnTracks的日志记录
@csrf_exempt
def manage_owntrack_log(request):
try:
# 从请求中读取并解析JSON数据
s = json.loads(request.read().decode('utf-8'))
# 提取必要的字段
tid = s['tid']
lat = s['lat']
lon = s['lon']
# 记录日志信息
logger.info(
'tid:{tid}.lat:{lat}.lon:{lon}'.format(
tid=tid, lat=lat, lon=lon))
# 验证字段是否存在
if tid and lat and lon:
# 创建并保存新的日志记录
m = OwnTrackLog()
m.tid = tid
m.lat = lat
m.lon = lon
m.save()
return HttpResponse('ok') # 返回成功响应
else:
return HttpResponse('data error') # 数据不完整错误响应
except Exception as e:
# 记录异常信息
logger.error(e)
return HttpResponse('error') # 异常错误响应
# 需要登录才能访问的视图,用于显示地图
@login_required
def show_maps(request):
# 仅允许超级用户访问
if request.user.is_superuser:
# 获取当前UTC日期作为默认日期
defaultdate = str(datetime.datetime.now(timezone.utc).date())
# 从请求参数中获取日期,默认为当前日期
date = request.GET.get('date', defaultdate)
# 准备上下文数据
context = {
'date': date
}
# 渲染模板并返回
return render(request, 'owntracks/show_maps.html', context)
else:
# 非超级用户返回403禁止访问
from django.http import HttpResponseForbidden
return HttpResponseForbidden()
# 需要登录才能访问的视图,用于显示日志日期列表
@login_required
def show_log_dates(request):
# 获取所有记录的创建时间
dates = OwnTrackLog.objects.values_list('creation_time', flat=True)
# 提取日期部分并去重排序
results = list(sorted(set(map(lambda x: x.strftime('%Y-%m-%d'), dates))))
# 准备上下文数据
context = {
'results': results
}
# 渲染模板并返回
return render(request, 'owntracks/show_log_dates.html', context)
# 将GPS坐标转换为高德地图坐标的函数
def convert_to_amap(locations):
convert_result = []
# 创建迭代器
it = iter(locations)
# 每次处理30个坐标高德API限制
item = list(itertools.islice(it, 30))
while item:
# 格式化坐标为"经度,纬度;经度,纬度"形式
datas = ';'.join(
set(map(lambda x: str(x.lon) + ',' + str(x.lat), item)))
# 高德API密钥和接口地址
key = '8440a376dfc9743d8924bf0ad141f28e'
api = 'http://restapi.amap.com/v3/assistant/coordinate/convert'
# 请求参数
query = {
'key': key,
'locations': datas,
'coordsys': 'gps' # 源坐标系统为GPS
}
# 发送转换请求
rsp = requests.get(url=api, params=query)
result = json.loads(rsp.text)
# 处理转换结果
if "locations" in result:
convert_result.append(result['locations'])
# 处理下一批坐标
item = list(itertools.islice(it, 30))
# 合并所有转换结果并返回
return ";".join(convert_result)
# 需要登录才能访问的视图,用于获取轨迹数据
@login_required
def get_datas(request):
# 获取当前UTC时间
now = django.utils.timezone.now().replace(tzinfo=timezone.utc)
# 默认查询日期为今天
querydate = django.utils.timezone.datetime(
now.year, now.month, now.day, 0, 0, 0)
# 如果请求中指定了日期,则使用指定日期
if request.GET.get('date', None):
date = list(map(lambda x: int(x), request.GET.get('date').split('-')))
querydate = django.utils.timezone.datetime(
date[0], date[1], date[2], 0, 0, 0)
# 计算查询日期的下一天(用于范围查询)
nextdate = querydate + datetime.timedelta(days=1)
# 查询指定日期范围内的所有记录
models = OwnTrackLog.objects.filter(
creation_time__range=(querydate, nextdate))
result = list()
# 如果有查询结果
if models and len(models):
# 按tid分组处理记录
for tid, item in groupby(
sorted(models, key=lambda k: k.tid), key=lambda k: k.tid):
# 构建返回数据结构
d = dict()
d["name"] = tid
paths = list()
# 注释掉的代码:使用高德转换后的经纬度
# locations = convert_to_amap(
# sorted(item, key=lambda x: x.creation_time))
# for i in locations.split(';'):
# paths.append(i.split(','))
# 使用GPS原始经纬度
# 按创建时间排序并添加到路径列表
for location in sorted(item, key=lambda x: x.creation_time):
paths.append([str(location.lon), str(location.lat)])
d["path"] = paths
result.append(d)
# 返回JSON格式的结果
return JsonResponse(result, safe=False)

@ -0,0 +1,184 @@
{% load blog_tags %} <!-- 加载自定义博客相关模板标签 -->
{% load i18n %} <!-- 加载国际化标签,支持多语言翻译 -->
<!-- 网站侧边栏容器role="complementary"标识为辅助内容区域 -->
<div id="secondary" class="widget-area" role="complementary">
<!-- 搜索框组件 -->
<aside id="search-2" class="widget widget_search">
<!-- 搜索表单请求方式为GET提交到/search路径 -->
<form role="search" method="get" id="searchform" class="searchform" action="/search">
<div>
<!-- 供屏幕阅读器识别的搜索标签,视觉上隐藏 -->
<label class="screen-reader-text" for="s">{% trans 'search' %}</label>
<!-- 搜索输入框name为"q"用于后端接收搜索关键词 -->
<input type="text" value="" name="q" id="q"/>
<!-- 搜索提交按钮 -->
<input type="submit" id="searchsubmit" />
</div>
</form>
</aside>
<!-- 额外侧边栏内容(若存在) -->
{% if extra_sidebars %}
{% for sidebar in extra_sidebars %} <!-- 循环遍历所有额外侧边栏 -->
<!-- 自定义HTML侧边栏组件 -->
<aside class="widget_text widget widget_custom_html">
<p class="widget-title">{{ sidebar.name }}</p> <!-- 侧边栏标题 -->
<div class="textwidget custom-html-widget">
<!-- 渲染侧边栏内容经过自定义markdown处理并允许安全HTML -->
{{ sidebar.content|custom_markdown|safe }}
</div>
</aside>
{% endfor %}
{% endif %}
<!-- 热门阅读文章(若存在) -->
{% if most_read_articles %}
<aside id="views-4" class="widget widget_views">
<p class="widget-title">Views</p> <!-- 组件标题(阅读量) -->
<ul>
{% for a in most_read_articles %} <!-- 循环遍历热门文章 -->
<li>
<!-- 文章链接,标题为文章标题 -->
<a href="{{ a.get_absolute_url }}" title="{{ a.title }}">
{{ a.title }}
</a> - {{ a.views }} views <!-- 显示文章阅读量 -->
</li>
{% endfor %}
</ul>
</aside>
{% endif %}
<!-- 文章分类侧边栏(若存在) -->
{% if sidebar_categorys %}
<aside id="su_siloed_terms-2" class="widget widget_su_siloed_terms">
<p class="widget-title">{% trans 'category' %}</p> <!-- 分类标题(多语言) -->
<ul>
{% for c in sidebar_categorys %} <!-- 循环遍历分类 -->
<li class="cat-item cat-item-184">
<!-- 分类链接,指向分类详情页 -->
<a href={{ c.get_absolute_url }}>{{ c.name }}</a>
</li>
{% endfor %}
</ul>
</aside>
{% endif %}
<!-- 最新评论(若存在且网站允许评论) -->
{% if sidebar_comments and open_site_comment %}
<aside id="ds-recent-comments-4" class="widget ds-widget-recent-comments">
<p class="widget-title">{% trans 'recent comments' %}</p> <!-- 最新评论标题 -->
<ul id="recentcomments">
{% for c in sidebar_comments %} <!-- 循环遍历最新评论 -->
<li class="recentcomments">
<span class="comment-author-link">{{ c.author.username }}</span> <!-- 评论作者 -->
{% trans 'published on' %}《 <!-- 翻译为“发表于” -->
<!-- 链接到评论所在的文章,锚点定位到具体评论 -->
<a href="{{ c.article.get_absolute_url }}#comment-{{ c.pk }}">{{ c.article.title }}</a>
</li>
{% endfor %}
</ul>
</aside>
{% endif %}
<!-- 最新文章(若存在) -->
{% if recent_articles %}
<aside id="recent-posts-2" class="widget widget_recent_entries">
<p class="widget-title">{% trans 'recent articles' %}</p> <!-- 最新文章标题 -->
<ul>
{% for a in recent_articles %} <!-- 循环遍历最新文章 -->
<li>
<!-- 文章链接,标题为文章标题 -->
<a href="{{ a.get_absolute_url }}" title="{{ a.title }}">
{{ a.title }}
</a>
</li>
{% endfor %}
</ul>
</aside>
{% endif %}
<!-- 收藏链接(若存在) -->
{% if sidabar_links %}
<aside id="linkcat-0" class="widget widget_links">
<p class="widget-title">{% trans 'bookmark' %}</p> <!-- 收藏标题 -->
<ul class='xoxo blogroll'>
{% for l in sidabar_links %} <!-- 循环遍历收藏链接 -->
<li>
<!-- 收藏链接,新窗口打开,标题为链接名称 -->
<a href="{{ l.link }}" target="_blank" title="{{ l.name }}">{{ l.name }}</a>
</li>
{% endfor %}
</ul>
</aside>
{% endif %}
<!-- Google广告若开启 -->
{% if show_google_adsense %}
<aside id="text-2" class="widget widget_text">
<p class="widget-title">Google AdSense</p> <!-- 广告标题 -->
<div class="textwidget">
<!-- 渲染Google广告代码允许安全HTML -->
{{ google_adsense_codes|safe }}
</div>
</aside>
{% endif %}
<!-- 标签云(若存在) -->
{% if sidebar_tags %}
<aside id="tag_cloud-2" class="widget widget_tag_cloud">
<p class="widget-title">{% trans 'Tag Cloud' %}</p> <!-- 标签云标题 -->
<div class="tagcloud">
{% for tag,count,size in sidebar_tags %} <!-- 循环遍历标签,获取标签、数量、字体大小 -->
<!-- 标签链接字体大小由size变量控制标题显示标签下文章数量 -->
<a href="{{ tag.get_absolute_url }}"
class="tag-link-{{ tag.id }} tag-link-position-{{ tag.id }}"
style="font-size: {{ size }}pt;" title="{{ count }}个话题"> {{ tag.name }}
</a>
{% endfor %}
</div>
</aside>
{% endif %}
<!-- 网站源码Star/Fork提示 -->
<aside id="text-2" class="widget widget_text">
<p class="widget-title">{% trans 'Welcome to star or fork the source code of this site' %}</p> <!-- 多语言提示文本 -->
<div class="textwidget">
<p>
<!-- GitHub Star按钮图片链接 -->
<a href="https://github.com/liangliangyy/DjangoBlog" rel="nofollow"><img
src="https://resource.lylinux.net/img.shields.io/github/stars/liangliangyy/djangoblog.svg?style=social&amp;label=Star"
alt="GitHub stars"></a>
<!-- GitHub Fork按钮图片链接 -->
<a href="https://github.com/liangliangyy/DjangoBlog" rel="nofollow"><img
src="https://resource.lylinux.net/img.shields.io/github/forks/liangliangyy/djangoblog.svg?style=social&amp;label=Fork"
alt="GitHub forks"></a>
</p>
</div>
</aside>
<!-- 功能链接侧边栏 -->
<aside id="meta-3" class="widget widget_meta">
<p class="widget-title">{% trans 'Function' %}</p> <!-- 功能标题 -->
<ul>
<!-- 后台管理系统链接 -->
<li><a href="/admin/" rel="nofollow">{% trans 'management site' %}</a></li>
{% if user.is_authenticated %} <!-- 若用户已登录 -->
<!-- 登出链接 -->
<li><a href="{% url "account:logout" %}" rel="nofollow">{% trans 'logout' %}</a></li>
{% else %}
<!-- 登录链接 -->
<li><a href="{% url "account:login" %}" rel="nofollow">{% trans 'login' %}</a></li>
{% endif %}
{% if user.is_superuser %} <!-- 若用户是超级管理员 -->
<!-- 轨迹记录链接(新窗口打开) -->
<li><a href="{% url 'owntracks:show_dates' %}" target="_blank">{% trans 'Track record' %}</a></li>
{% endif %}
<!-- GitBook链接新窗口打开 -->
<li><a href="http://gitbook.lylinux.net" target="_blank" rel="nofollow">GitBook</a></li>
</ul>
</aside>
<!-- 回到顶部按钮,初始显示,鼠标悬停提示文本 -->
<div id="rocket" class="show" title="{% trans 'Click me to return to the top' %}"></div>
</div><!-- #secondary --> <!-- 侧边栏容器结束 -->

@ -0,0 +1,289 @@
from django.test import Client, RequestFactory, TestCase
from django.urls import reverse
# 导入时区处理模块,用于处理时间相关数据
from django.utils import timezone
# 导入国际化翻译函数,用于多语言文本
from django.utils.translation import gettext_lazy as _
# 导入用户模型,用于创建测试用户数据
from accounts.models import BlogUser
# 导入文章、分类模型,用于创建测试内容数据
from blog.models import Article, Category
# 导入项目工具函数,用于测试通用功能
from djangoblog.utils import *
# 导入当前应用accounts的工具函数用于测试账号相关工具功能
from . import utils
# 定义账号功能测试类继承TestCase基础测试用例类
class AccountTest(TestCase):
# 测试前初始化方法,每个测试方法执行前自动运行
def setUp(self):
# 初始化测试客户端用于模拟用户发起HTTP请求
self.client = Client()
# 初始化请求工厂,用于构造自定义请求对象
self.factory = RequestFactory()
# 创建普通测试用户,存入测试数据库
self.blog_user = BlogUser.objects.create_user(
username="test", # 用户名
email="admin@admin.com", # 邮箱
password="12345678" # 密码
)
# 定义测试用的新密码字符串,用于后续密码修改测试
self.new_test = "xxx123--="
# 测试账号验证功能(登录、管理员权限、文章管理)
def test_validate_account(self):
# 获取当前站点域名(用于测试环境下的域名相关逻辑)
site = get_current_site().domain
# 创建超级用户,用于测试管理员权限
user = BlogUser.objects.create_superuser(
email="liangliangyy1@gmail.com", # 超级用户邮箱
username="liangliangyy1", # 超级用户名
password="qwer!@#$ggg") # 超级用户密码
# 从数据库中查询刚创建的超级用户,用于后续验证
testuser = BlogUser.objects.get(username='liangliangyy1')
# 模拟超级用户登录,返回登录结果(布尔值)
loginresult = self.client.login(
username='liangliangyy1', # 登录用户名
password='qwer!@#$ggg') # 登录密码
# 断言登录结果应为True登录成功
self.assertEqual(loginresult, True)
# 模拟超级用户访问管理员后台首页
response = self.client.get('/admin/')
# 断言响应状态码应为200访问成功
self.assertEqual(response.status_code, 200)
# 创建测试分类,用于后续文章关联
category = Category()
category.name = "categoryaaa" # 分类名称
category.creation_time = timezone.now() # 分类创建时间(当前时间)
category.last_modify_time = timezone.now() # 分类最后修改时间(当前时间)
category.save() # 保存分类到测试数据库
# 创建测试文章,关联上述分类和超级用户
article = Article()
article.title = "nicetitleaaa" # 文章标题
article.body = "nicecontentaaa" # 文章内容
article.author = user # 文章作者(超级用户)
article.category = category # 文章所属分类
article.type = 'a' # 文章类型(假设'a'代表普通文章)
article.status = 'p' # 文章状态(假设'p'代表已发布)
article.save() # 保存文章到测试数据库
# 模拟访问该文章的管理员编辑页通过文章模型的自定义方法获取URL
response = self.client.get(article.get_admin_url())
# 断言响应状态码应为200管理员有权限访问访问成功
self.assertEqual(response.status_code, 200)
# 测试账号注册功能(注册、邮箱验证、登录、权限提升、文章管理、登出)
def test_validate_register(self):
# 断言:数据库中初始不存在邮箱为'user123@user.com'的用户计数为0
self.assertEquals(
0, len(
BlogUser.objects.filter(
email='user123@user.com')))
# 模拟POST请求提交注册表单访问注册接口
response = self.client.post(reverse('account:register'), {
'username': 'user1233', # 注册用户名
'email': 'user123@user.com', # 注册邮箱
'password1': 'password123!q@wE#R$T', # 注册密码
'password2': 'password123!q@wE#R$T', # 密码确认(与密码一致)
})
# 断言注册后数据库中应存在该邮箱用户计数为1
self.assertEquals(
1, len(
BlogUser.objects.filter(
email='user123@user.com')))
# 从数据库中查询刚注册的用户
user = BlogUser.objects.filter(email='user123@user.com')[0]
# 生成用户邮箱验证的签名双重SHA256加密结合密钥和用户ID
sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
# 反向解析验证结果页的URL
path = reverse('accounts:result')
# 拼接完整的邮箱验证URL包含用户ID和签名
url = '{path}?type=validation&id={id}&sign={sign}'.format(
path=path, id=user.id, sign=sign)
# 模拟访问邮箱验证URL完成验证
response = self.client.get(url)
# 断言验证页面访问成功状态码200
self.assertEqual(response.status_code, 200)
# 模拟刚注册的用户登录
self.client.login(username='user1233', password='password123!q@wE#R$T')
# 重新查询该用户,准备提升权限
user = BlogUser.objects.filter(email='user123@user.com')[0]
user.is_superuser = True # 设置为超级用户
user.is_staff = True # 设置为管理员有权访问admin后台
user.save() # 保存权限修改
# 调用工具函数删除侧边栏缓存(避免缓存影响测试结果)
delete_sidebar_cache()
# 创建测试分类(用于后续文章关联)
category = Category()
category.name = "categoryaaa" # 分类名称
category.creation_time = timezone.now() # 创建时间
category.last_modify_time = timezone.now() # 最后修改时间
category.save() # 保存分类
# 创建测试文章(关联上述分类和提升权限后的用户)
article = Article()
article.category = category # 所属分类
article.title = "nicetitle333" # 文章标题
article.body = "nicecontentttt" # 文章内容
article.author = user # 文章作者(提升权限后的用户)
article.type = 'a' # 文章类型
article.status = 'p' # 文章状态(已发布)
article.save() # 保存文章
# 模拟访问该文章的管理员编辑页
response = self.client.get(article.get_admin_url())
# 断言访问成功状态码200因用户已提升为管理员
self.assertEqual(response.status_code, 200)
# 模拟用户登出(访问登出接口)
response = self.client.get(reverse('account:logout'))
# 断言:登出响应状态码在[301,302,200]内(重定向或成功)
self.assertIn(response.status_code, [301, 302, 200])
# 登出后再次访问文章管理员编辑页(应无权限)
response = self.client.get(article.get_admin_url())
# 断言:响应状态码在[301,302,200]内(可能重定向到登录页)
self.assertIn(response.status_code, [301, 302, 200])
# 模拟使用错误密码登录(密码不匹配)
response = self.client.post(reverse('account:login'), {
'username': 'user1233', # 正确用户名
'password': 'password123' # 错误密码
})
# 断言:登录响应状态码在[301,302,200]内(登录失败可能重定向或返回表单)
self.assertIn(response.status_code, [301, 302, 200])
# 错误登录后访问文章管理员编辑页(仍无权限)
response = self.client.get(article.get_admin_url())
# 断言:响应状态码在[301,302,200]内(可能重定向到登录页)
self.assertIn(response.status_code, [301, 302, 200])
# 测试邮箱验证码的生成、存储、发送和验证功能
def test_verify_email_code(self):
# 定义测试邮箱地址
to_email = "admin@admin.com"
# 生成随机邮箱验证码(调用工具函数)
code = generate_code()
# 存储验证码(关联邮箱和验证码,用于后续验证)
utils.set_code(to_email, code)
# 发送验证邮件(调用工具函数,将验证码发送到测试邮箱)
utils.send_verify_email(to_email, code)
# 验证:使用正确邮箱和正确验证码
err = utils.verify("admin@admin.com", code)
# 断言验证无错误返回None
self.assertEqual(err, None)
# 验证:使用错误邮箱和正确验证码
err = utils.verify("admin@123.com", code)
# 断言:验证错误,错误类型为字符串(返回错误信息)
self.assertEqual(type(err), str)
# 测试“忘记密码-发送验证码”功能的成功场景
def test_forget_password_email_code_success(self):
# 模拟POST请求提交邮箱访问“发送忘记密码验证码”接口
resp = self.client.post(
path=reverse("account:forget_password_code"), # 反向解析接口URL
data=dict(email="admin@admin.com") # 提交已存在的测试邮箱
)
# 断言响应状态码为200请求处理成功
self.assertEqual(resp.status_code, 200)
# 断言:响应内容为"ok"(表示验证码发送成功)
self.assertEqual(resp.content.decode("utf-8"), "ok")
# 测试“忘记密码-发送验证码”功能的失败场景
def test_forget_password_email_code_fail(self):
# 模拟POST请求不提交邮箱空数据
resp = self.client.post(
path=reverse("account:forget_password_code"),
data=dict() # 空数据
)
# 断言:响应内容为“错误的邮箱”(无邮箱参数,请求失败)
self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱")
# 模拟POST请求提交格式错误的邮箱无效邮箱
resp = self.client.post(
path=reverse("account:forget_password_code"),
data=dict(email="admin@com") # 格式错误的邮箱
)
# 断言:响应内容为“错误的邮箱”(邮箱格式无效,请求失败)
self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱")
# 测试“忘记密码-重置密码”功能的成功场景
def test_forget_password_email_success(self):
# 生成随机验证码
code = generate_code()
# 存储验证码(关联测试用户的邮箱)
utils.set_code(self.blog_user.email, code)
# 构造重置密码的请求数据
data = dict(
new_password1=self.new_test, # 新密码
new_password2=self.new_test, # 新密码确认(与新密码一致)
email=self.blog_user.email, # 测试用户邮箱
code=code, # 正确的验证码
)
# 模拟POST请求提交重置密码数据访问重置密码接口
resp = self.client.post(
path=reverse("account:forget_password"), # 反向解析接口URL
data=data
)
# 断言响应状态码为302重置成功重定向到登录页或结果页
self.assertEqual(resp.status_code, 302)
# 验证:数据库中用户密码是否已更新
blog_user = BlogUser.objects.filter(
email=self.blog_user.email, # 按邮箱查询测试用户
).first() # 获取查询结果的第一个(唯一用户)
# 断言:查询到用户(用户存在)
self.assertNotEqual(blog_user, None)
# 断言用户密码与新密码匹配check_password方法验证哈希密码
self.assertEqual(blog_user.check_password(data["new_password1"]), True)
# 测试“忘记密码-重置密码”功能:邮箱不存在的失败场景
def test_forget_password_email_not_user(self):
# 构造重置密码请求数据(使用不存在的邮箱)
data = dict(
new_password1=self.new_test, # 新密码
new_password2=self.new_test, # 新密码确认
email="123@123.com", # 不存在的邮箱
code="123456", # 任意验证码
)
# 模拟POST请求提交数据访问重置密码接口
resp = self.client.post(
path=reverse("account:forget_password"),
data=data
)
# 断言响应状态码为200请求处理完成但重置失败返回表单页
self.assertEqual(resp.status_code, 200)
# 测试“忘记密码-重置密码”功能:验证码错误的失败场景
def test_forget_password_email_code_error(self):
# 生成正确的验证码并存储(关联测试用户邮箱)
code = generate_code()
utils.set_code(self.blog_user.email, code)
# 构造重置密码请求数据(使用错误的验证码)
data = dict(
new_password1=self.new_test, # 新密码
new_password2=self.new_test, # 新密码确认
email=self.blog_user.email, # 正确的测试用户邮箱
code="111111", # 错误的验证码
)
# 模拟POST请求提交数据访问重置密码接口
resp = self.client.post(
path=reverse("account:forget_password"),
data=data
)
# 断言响应状态码为200请求处理完成但验证码错误返回表单页
self.assertEqual(resp.status_code, 200)

@ -0,0 +1,17 @@
from django.urls import path
# 导入当前应用comments的views模块用于关联视图函数/类
from . import views
# 定义当前应用的命名空间为"comments"避免URL名称冲突
app_name = "comments"
# 定义URL路由列表存储URL规则与视图的映射关系
urlpatterns = [
path(
'article/<int:article_id>/postcomment', # URL路径包含文章ID整数类型的动态路径
views.CommentPostView.as_view(), # 关联的视图类调用CommentPostView的as_view()方法生成视图函数
name='postcomment'), # 给该URL命名为"postcomment",用于反向解析
]

@ -0,0 +1,59 @@
# 导入logging模块用于记录日志如异常信息
import logging
# 导入国际化翻译函数,用于生成多语言邮件内容
from django.utils.translation import gettext_lazy as _
# 导入项目工具函数,获取当前站点域名
from djangoblog.utils import get_current_site
# 导入项目工具函数,用于发送邮件
from djangoblog.utils import send_email
# 创建当前模块的日志记录器,用于记录该函数的运行日志
logger = logging.getLogger(__name__)
# 定义发送评论相关邮件的函数,接收评论对象作为参数
def send_comment_email(comment):
# 获取当前站点的域名(用于拼接文章链接)
site = get_current_site().domain
# 定义邮件主题(多语言翻译,如中文为“感谢您的评论”)
subject = _('Thanks for your comment')
# 拼接评论所属文章的完整URLHTTPS协议 + 域名 + 文章相对路径)
article_url = f"https://{site}{comment.article.get_absolute_url()}"
# 定义邮件HTML内容多语言模板包含感谢语、文章链接、链接提示
# 使用字符串格式化,替换{article_url}和{article_title}为实际值
html_content = _("""<p>Thank you very much for your comments on this site</p>
You can visit <a href="%(article_url)s" rel="bookmark">%(article_title)s</a>
to review your comments,
Thank you again!
<br />
If the link above cannot be opened, please copy this link to your browser.
%(article_url)s""") % {'article_url': article_url, 'article_title': comment.article.title}
tomail = comment.author.email
# 调用send_email函数发送邮件收件人列表、主题、HTML内容
send_email([tomail], subject, html_content)
# 尝试给父评论作者发送“评论被回复”的邮件(若当前评论是回复)
try:
# 判断当前评论是否有父评论(即是否为回复)
if comment.parent_comment:
# 定义回复通知的HTML邮件内容多语言模板
# 包含父评论所属文章链接、父评论内容、查看提示
html_content = _("""Your comment on <a href="%(article_url)s" rel="bookmark">%(article_title)s</a><br/> has
received a reply. <br/> %(comment_body)s
<br/>
go check it out!
<br/>
If the link above cannot be opened, please copy this link to your browser.
%(article_url)s
""") % {'article_url': article_url, 'article_title': comment.article.title,
'comment_body': comment.parent_comment.body}
# 获取父评论作者的邮箱(回复通知的收件人)
tomail = comment.parent_comment.author.email
# 发送回复通知邮件
send_email([tomail], subject, html_content)
# 捕获所有异常,避免发送失败影响主流程
except Exception as e:
logger.error(e)

@ -0,0 +1,98 @@
from django.core.exceptions import ValidationError
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404
# 导入方法装饰器工具,用于装饰类中的方法
from django.utils.decorators import method_decorator
# 导入CSRF保护装饰器防止跨站请求伪造
from django.views.decorators.csrf import csrf_protect
# 导入表单视图基类,用于处理表单提交逻辑
from django.views.generic.edit import FormView
# 导入用户模型,用于获取评论作者信息
from accounts.models import BlogUser
# 导入文章模型,用于关联评论所属文章
from blog.models import Article
# 导入评论表单类,用于处理评论提交数据
from .forms import CommentForm
# 导入评论模型,用于创建和保存评论
from .models import Comment
# 定义评论提交视图类继承自FormView表单处理基类
class CommentPostView(FormView):
form_class = CommentForm # 指定使用的表单类为CommentForm
template_name = 'blog/article_detail.html' # 指定表单验证失败时渲染的模板
# 使用CSRF保护装饰器装饰dispatch方法确保表单提交安全
@method_decorator(csrf_protect)
def dispatch(self, *args, **kwargs):
# 调用父类的dispatch方法处理请求分发
return super(CommentPostView, self).dispatch(*args, **kwargs)
# 处理GET请求重定向到文章详情页的评论区
def get(self, request, *args, **kwargs):
# 从URL参数中获取文章ID
article_id = self.kwargs['article_id']
# 获取对应的文章对象不存在则返回404
article = get_object_or_404(Article, pk=article_id)
# 获取文章详情页的URL
url = article.get_absolute_url()
# 重定向到文章详情页的评论区(通过锚点#comments定位
return HttpResponseRedirect(url + "#comments")
# 处理表单验证失败的逻辑
def form_invalid(self, form):
# 从URL参数中获取文章ID
article_id = self.kwargs['article_id']
# 获取对应的文章对象
article = get_object_or_404(Article, pk=article_id)
# 渲染文章详情页模板,传递错误的表单和文章对象(用于显示错误信息)
return self.render_to_response({
'form': form,
'article': article
})
# 处理表单验证成功后的逻辑
def form_valid(self, form):
"""提交的数据验证合法后的逻辑"""
# 获取当前登录用户
user = self.request.user
# 根据用户ID获取对应的用户对象评论作者
author = BlogUser.objects.get(pk=user.pk)
# 从URL参数中获取文章ID
article_id = self.kwargs['article_id']
# 获取对应的文章对象
article = get_object_or_404(Article, pk=article_id)
# 检查文章是否允许评论:若文章评论状态为关闭或文章状态为草稿,则抛出验证错误
if article.comment_status == 'c' or article.status == 'c':
raise ValidationError("该文章评论已关闭.")
# 保存表单数据但不提交到数据库(获取评论对象)
comment = form.save(False)
# 关联评论到对应的文章
comment.article = article
# 导入工具函数,获取博客设置
from djangoblog.utils import get_blog_setting
settings = get_blog_setting()
# 若博客设置为评论无需审核,则直接启用评论
if not settings.comment_need_review:
comment.is_enable = True
# 设置评论的作者
comment.author = author
# 处理回复功能若存在父评论ID则关联到父评论
if form.cleaned_data['parent_comment_id']:
# 根据父评论ID获取父评论对象
parent_comment = Comment.objects.get(
pk=form.cleaned_data['parent_comment_id'])
# 设置当前评论的父评论
comment.parent_comment = parent_comment
# 保存评论到数据库(执行真正的保存操作)
comment.save(True)
# 重定向到文章详情页的当前评论位置(通过锚点#div-comment-{评论ID}定位)
return HttpResponseRedirect(
"%s#div-comment-%d" %
(article.get_absolute_url(), comment.pk))
Loading…
Cancel
Save