Compare commits

..

5 Commits

BIN
.gitignore vendored

Binary file not shown.

@ -0,0 +1,15 @@
<?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>

@ -0,0 +1,68 @@
<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>

@ -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" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.12" 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/django.iml" filepath="$PROJECT_DIR$/.idea/django.iml" />
</modules>
</component>
</project>

@ -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>

@ -4,7 +4,11 @@
<option name="autoReloadType" value="SELECTIVE" />
</component>
<component name="ChangeListManager">
<list default="true" id="feebdecb-aab4-468a-89d2-02ab184d6524" name="更改" comment="注释" />
<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>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
@ -13,7 +17,7 @@
<component name="Git.Settings">
<option name="RECENT_BRANCH_BY_REPOSITORY">
<map>
<entry key="$PROJECT_DIR$" value="gjw_branch" />
<entry key="$PROJECT_DIR$" value="main" />
</map>
</option>
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
@ -34,7 +38,6 @@
"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",

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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

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

@ -0,0 +1,12 @@
<?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> <!-- 模块定义结束 -->

@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings><!-- 配置设置区域 -->
<option name="USE_PROJECT_PROFILE" value="false" /><!-- 配置选项是否使用项目配置文件当前设置为不使用false -->
<version value="1.0" /><!-- 配置版本信息当前版本为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 (pythonProject)" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.12" 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/DjangoBlog-master.iml" filepath="$PROJECT_DIR$/.idea/DjangoBlog-master.iml" />
</modules>
</component>
</project>

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

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

@ -8,20 +8,14 @@ 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")
@ -30,40 +24,29 @@ 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',
@ -72,7 +55,5 @@ 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
class AccountsConfig(AppConfig):
name = 'accounts'

@ -8,37 +8,28 @@ 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():
@ -46,15 +37,11 @@ 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(
@ -65,7 +52,6 @@ class ForgetPasswordForm(forms.Form):
),
)
# xm: 确认新密码字段
new_password2 = forms.CharField(
label="确认密码",
widget=forms.PasswordInput(
@ -76,7 +62,6 @@ class ForgetPasswordForm(forms.Form):
),
)
# xm: 邮箱输入字段,用于验证用户身份
email = forms.EmailField(
label='邮箱',
widget=forms.TextInput(
@ -87,7 +72,6 @@ class ForgetPasswordForm(forms.Form):
),
)
# xm: 验证码输入字段
code = forms.CharField(
label=_('Code'),
widget=forms.TextInput(
@ -98,17 +82,15 @@ 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(
@ -118,7 +100,6 @@ 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(
@ -130,9 +111,7 @@ class ForgetPasswordForm(forms.Form):
return code
# xm: 获取忘记密码验证码的表单
class ForgetPasswordCodeForm(forms.Form):
# xm: 邮箱字段,用于发送验证码
email = forms.EmailField(
label=_('Email'),
)

@ -18,52 +18,30 @@ 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,40 +11,33 @@ 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',

@ -0,0 +1,53 @@
# 导入Django内置的用户模型基类用于扩展用户功能
from django.contrib.auth.models import AbstractUser
# 导入Django的模型模块用于定义数据库模型
from django.db import models
# 导入reverse函数用于生成URL
from django.urls import reverse
# 导入时区相关函数,用于处理时间字段
from django.utils.timezone import now
# 导入翻译相关工具,用于国际化支持
from django.utils.translation import gettext_lazy as _
# 导入自定义工具函数,用于获取当前站点信息
from djangoblog.utils import get_current_site
# 创建模型类
class BlogUser(AbstractUser):
# 昵称字段允许为空max_length指定最大长度_()用于支持国际化翻译
nickname = models.CharField(_('nick name'), max_length=100, blank=True)
# 账号创建时间字段,默认值为当前时间
creation_time = models.DateTimeField(_('creation time'), default=now)
# 最后修改时间字段,默认值为当前时间(通常需要在保存时更新,可能在其他地方处理)
last_modify_time = models.DateTimeField(_('last modify time'), default=now)
# 账号创建来源字段,用于记录用户注册渠道,允许为空
source = models.CharField(_('create source'), max_length=100, blank=True)
def get_absolute_url(self):
return reverse(
'blog:author_detail', # URL配置中定义的名称
kwargs={'author_name': self.username} # 传递的参数,使用用户名作为标识
)
def __str__(self):
return self.email
def get_full_url(self):
# 获取当前站点的域名(从站点配置中获取)
site = get_current_site().domain
# 拼接域名和路径形成完整URL
url = "https://{site}{path}".format(
site=site,
path=self.get_absolute_url()
)
return url
class Meta:
# 排序方式按id降序排列新创建的用户在前
ordering = ['-id']
# 模型的显示名称(单数),支持国际化
verbose_name = _('user')
# 模型的显示名称(复数),支持国际化
verbose_name_plural = verbose_name
# 指定获取最新记录时使用的字段
get_latest_by = 'id'

@ -11,13 +11,10 @@ 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",
@ -25,33 +22,27 @@ 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"
@ -61,53 +52,44 @@ 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"
@ -118,45 +100,36 @@ 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"),
@ -166,7 +139,6 @@ 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"),
@ -180,7 +152,6 @@ 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)
@ -196,14 +167,13 @@ 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,
@ -218,7 +188,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,28 @@
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'),
]

@ -2,34 +2,25 @@ 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,7 +10,6 @@ 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:
@ -18,15 +17,12 @@ 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:
@ -38,22 +34,16 @@ 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,26 +30,22 @@ 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:
@ -58,7 +54,6 @@ 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>
@ -69,7 +64,6 @@ class RegisterView(FormView):
如果上面链接无法打开请将此链接复制至浏览器
{url}
""".format(url=url)
# xm: 发送验证邮件
send_email(
emailto=[
user.email,
@ -77,7 +71,6 @@ class RegisterView(FormView):
title='验证您的电子邮箱',
content=content)
# xm: 重定向到结果页面
url = reverse('accounts:result') + \
'?type=register&id=' + str(user.id)
return HttpResponseRedirect(url)
@ -87,24 +80,19 @@ 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'
@ -112,45 +100,41 @@ 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()]):
@ -158,30 +142,25 @@ 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 = '''
@ -196,15 +175,12 @@ 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()
@ -213,17 +189,14 @@ 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)

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

@ -0,0 +1,13 @@
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,49 +1,38 @@
# Generated by Django 4.1.7 on 2023-03-02 07:14
# gjw:导入Django内置模块
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):
#gjw:初始迁移文件
initial = True
#gjw:依赖关系需要先执行blog应用的0001_initial迁移和用户模型的迁移
dependencies = [
('blog', '0001_initial'), #gjw:依赖blog应用的初始迁移
migrations.swappable_dependency(settings.AUTH_USER_MODEL), #gjw:依赖可切换的用户模型
('blog', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
#gjw:迁移操作列表
operations = [
#gjw:创建Comment模型对应的数据库表
migrations.CreateModel(
name='Comment', #gjw:模型名称:评论
name='Comment',
fields=[
#gjw:主键字段自增BigAutoField
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
#gjw:评论正文TextField类型最大长度300字符
('body', models.TextField(max_length=300, verbose_name='正文')),
#gjw:创建时间,默认值为当前时间
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
#gjw:最后修改时间,默认值为当前时间
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
#gjw:是否启用/显示评论布尔字段默认True
('is_enable', models.BooleanField(default=True, verbose_name='是否显示')),
#gjw:外键关联到Article模型级联删除
('article', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.article', verbose_name='文章')),
#gjw:外键关联到用户模型,级联删除
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='作者')),
#gjw:自关联外键,用于实现评论的回复功能(父级评论)
#gjw:blank=True和null=True允许该字段为空表示顶级评论
('parent_comment', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='comments.comment', verbose_name='上级评论')),
],
#gjw:模型的元数据配置
options={
'verbose_name': '评论', #gjw:单数名称
'verbose_name_plural': '评论', #gjw:复数名称
'ordering': ['-id'], #gjw:默认按ID降序排列
'get_latest_by': 'id', #gjw:指定按ID字段获取最新记录
'verbose_name': '评论',
'verbose_name_plural': '评论',
'ordering': ['-id'],
'get_latest_by': 'id',
},
),
]
]

@ -0,0 +1,18 @@
# 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,6 +1,5 @@
# Generated by Django 4.2.5 on 2023-09-06 13:13
# gjw:导入Django相关模块
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
@ -8,80 +7,54 @@ import django.utils.timezone
class Migration(migrations.Migration):
# gjw:数据库迁移类
# gjw:依赖关系:需要先执行其他迁移文件
dependencies = [
# gjw:依赖可切换的用户模型
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
#gjw: 依赖blog应用的0005迁移文件
('blog', '0005_alter_article_options_alter_category_options_and_more'),
# gjw:依赖comments应用的0002迁移文件修改is_enable字段的迁移
('comments', '0002_alter_comment_is_enable'),
]
# gjw:迁移操作列表
operations = [
#gjw: 修改Comment模型的元数据选项
migrations.AlterModelOptions(
name='comment', # gjw:模型名称
options={
'get_latest_by': 'id', # gjw:指定按ID获取最新记录
'ordering': ['-id'], #gjw: 按ID降序排列
'verbose_name': 'comment', #gjw: 单数显示名称改为英文
'verbose_name_plural': 'comment', # gjw:复数显示名称改为英文
},
name='comment',
options={'get_latest_by': 'id', 'ordering': ['-id'], 'verbose_name': 'comment', 'verbose_name_plural': 'comment'},
),
# gjw:删除created_time字段
migrations.RemoveField(
model_name='comment',
name='created_time',
),
#gjw: 删除last_mod_time字段
migrations.RemoveField(
model_name='comment',
name='last_mod_time',
),
# gjw:新增creation_time字段
migrations.AddField(
model_name='comment',
name='creation_time',
# gjw:日期时间字段,默认值为当前时间
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
# gjw:新增last_modify_time字段
migrations.AddField(
model_name='comment',
name='last_modify_time',
# gjw:日期时间字段,默认值为当前时间
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='last modify time'),
),
# gjw:修改article字段的显示名称
migrations.AlterField(
model_name='comment',
name='article',
# gjw:外键关联到Article模型级联删除显示名称改为英文
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.article', verbose_name='article'),
),
#gjw: 修改author字段的显示名称
migrations.AlterField(
model_name='comment',
name='author',
# gjw:外键关联到用户模型,级联删除,显示名称改为英文
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='author'),
),
# gjw:修改is_enable字段的显示名称
migrations.AlterField(
model_name='comment',
name='is_enable',
# gjw:布尔字段默认False显示名称改为英文
field=models.BooleanField(default=False, verbose_name='enable'),
),
# gjw:修改parent_comment字段的显示名称
migrations.AlterField(
model_name='comment',
name='parent_comment',
# gjw:自关联外键,允许为空,级联删除,显示名称改为英文
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='comments.comment', verbose_name='parent comment'),
),
]
]

@ -0,0 +1,39 @@
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,93 +1,68 @@
from django.test import Client, RequestFactory, TransactionTestCase
from django.urls import reverse
from accounts.models import BlogUser
from blog.models import Category, Article
from comments.models import Comment
from comments.templatetags.comments_tags import * # gjw:导入评论模板标签
from comments.templatetags.comments_tags import *
from djangoblog.utils import get_max_articleid_commentid
# gjw:评论功能测试类
# Create your tests here.
class CommentsTest(TransactionTestCase):
"""
评论系统测试类
测试评论的发布回复显示等功能
使用TransactionTestCase确保测试数据库事务隔离
"""
def setUp(self):
"""
测试初始化方法在每个测试方法执行前运行
创建测试所需的用户文章和配置
"""
self.client = Client() # gjw:Django测试客户端用于模拟HTTP请求
self.factory = RequestFactory() #gjw: 请求工厂,用于创建请求对象
# gjw:设置博客配置:评论需要审核
self.client = Client()
self.factory = RequestFactory()
from blog.models import BlogSettings
value = BlogSettings()
value.comment_need_review = True # gjw:开启评论审核功能
value.comment_need_review = True
value.save()
# gjw:创建超级用户用于测试
self.user = BlogUser.objects.create_superuser(
email="liangliangyy1@gmail.com",
username="liangliangyy1",
password="liangliangyy1")
def update_article_comment_status(self, article):
"""
更新文章所有评论的启用状态
将文章的所有评论设置为启用状态用于测试
Args:
article: Article实例
"""
comments = article.comment_set.all() #gjw: 获取文章的所有评论
comments = article.comment_set.all()
for comment in comments:
comment.is_enable = True # gjw:启用评论
comment.is_enable = True
comment.save()
def test_validate_comment(self):
"""
测试评论验证功能
包括评论发布评论回复评论树解析等
"""
# gjw:登录测试用户
self.client.login(username='liangliangyy1', password='liangliangyy1')
# gjw:创建测试分类
category = Category()
category.name = "categoryccc"
category.save()
# gjw:创建测试文章
article = Article()
article.title = "nicetitleccc"
article.body = "nicecontentccc"
article.author = self.user
article.category = category
article.type = 'a' #gjw: 文章类型
article.status = 'p' # gjw:发布状态
article.type = 'a'
article.status = 'p'
article.save()
#gjw: 获取评论提交URL
comment_url = reverse(
'comments:postcomment', kwargs={
'article_id': article.id})
# gjw:测试提交第一条评论
response = self.client.post(comment_url,
{
'body': '123ffffffffff' # gjw:评论内容
'body': '123ffffffffff'
})
# gjw:验证重定向响应(评论提交后应该重定向)
self.assertEqual(response.status_code, 302)
# gjw:重新获取文章对象验证评论数量由于需要审核初始应为0
article = Article.objects.get(pk=article.pk)
self.assertEqual(len(article.comment_list()), 0)
# gjw:启用评论后再次验证
self.update_article_comment_status(article)
self.assertEqual(len(article.comment_list()), 1)
# gjw:测试提交第二条评论
response = self.client.post(comment_url,
{
'body': '123ffffffffff',
@ -95,15 +70,11 @@ class CommentsTest(TransactionTestCase):
self.assertEqual(response.status_code, 302)
#gjw: 验证第二条评论
article = Article.objects.get(pk=article.pk)
self.update_article_comment_status(article)
self.assertEqual(len(article.comment_list()), 2)
# gjw:获取第一条评论的ID用于回复测试
parent_comment_id = article.comment_list()[0].id
# gjw:测试提交回复评论(包含复杂内容)
response = self.client.post(comment_url,
{
'body': '''
@ -118,31 +89,21 @@ class CommentsTest(TransactionTestCase):
[ddd](http://www.baidu.com)
''', # gjw:包含Markdown格式的评论内容
'parent_comment_id': parent_comment_id # gjw:父评论ID
''',
'parent_comment_id': parent_comment_id
})
# gjw:验证回复评论提交
self.assertEqual(response.status_code, 302)
self.update_article_comment_status(article)
# gjw:验证评论总数
article = Article.objects.get(pk=article.pk)
self.assertEqual(len(article.comment_list()), 3)
# gjw:测试评论树解析功能
comment = Comment.objects.get(id=parent_comment_id)
tree = parse_commenttree(article.comment_list(), comment)
self.assertEqual(len(tree), 1) # gjw:验证评论树结构
#gjw: 测试评论项显示功能
self.assertEqual(len(tree), 1)
data = show_comment_item(comment, True)
self.assertIsNotNone(data)
# gjw:测试获取最大文章ID和评论ID
s = get_max_articleid_commentid()
self.assertIsNotNone(s)
#gjw: 测试评论邮件发送功能
from comments.utils import send_comment_email
send_comment_email(comment)
send_comment_email(comment)

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

@ -0,0 +1,38 @@
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)

@ -0,0 +1,63 @@
# 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))

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

Loading…
Cancel
Save