Compare commits

..

No commits in common. 'master' and 'onet' have entirely different histories.
master ... onet

BIN
.gitignore vendored

Binary file not shown.

@ -1,111 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AutoImportSettings">
<option name="autoReloadType" value="SELECTIVE" />
</component>
<component name="ChangeListManager">
<list default="true" id="feebdecb-aab4-468a-89d2-02ab184d6524" name="更改" comment="注释" />
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
<option name="LAST_RESOLUTION" value="IGNORE" />
</component>
<component name="Git.Settings">
<option name="RECENT_BRANCH_BY_REPOSITORY">
<map>
<entry key="$PROJECT_DIR$" value="gjw_branch" />
</map>
</option>
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
</component>
<component name="ProjectColorInfo">{
&quot;customColor&quot;: &quot;&quot;,
&quot;associatedIndex&quot;: 1
}</component>
<component name="ProjectId" id="33xBFgI9uhuAj9lNSHkgcMEmGEr" />
<component name="ProjectViewState">
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent"><![CDATA[{
"keyToString": {
"ModuleVcsDetector.initialDetectionPerformed": "true",
"RunOnceActivity.OpenDjangoStructureViewOnStart": "true",
"RunOnceActivity.ShowReadmeOnStart": "true",
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
"RunOnceActivity.git.unshallow": "true",
"git-widget-placeholder": "master",
"last_opened_file_path": "C:/Users/35500/Desktop/软件工程与方法学/django",
"node.js.detected.package.eslint": "true",
"node.js.detected.package.tslint": "true",
"node.js.selected.package.eslint": "(autodetect)",
"node.js.selected.package.tslint": "(autodetect)",
"nodejs_package_manager_path": "npm",
"vue.rearranger.settings.migration": "true"
}
}]]></component>
<component name="RunManager">
<configuration name="django" type="Python.DjangoServer" factoryName="Django server">
<module name="django" />
<option name="ENV_FILES" value="" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="PARENT_ENVS" value="true" />
<envs>
<env name="PYTHONUNBUFFERED" value="1" />
</envs>
<option name="SDK_HOME" value="" />
<option name="WORKING_DIRECTORY" value="" />
<option name="IS_MODULE_SDK" value="false" />
<option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" />
<option name="launchJavascriptDebuger" value="false" />
<option name="port" value="8000" />
<option name="host" value="" />
<option name="additionalOptions" value="" />
<option name="browserUrl" value="" />
<option name="runTestServer" value="false" />
<option name="runNoReload" value="false" />
<option name="useCustomRunCommand" value="false" />
<option name="customRunCommand" value="" />
<method v="2" />
</configuration>
</component>
<component name="SharedIndexes">
<attachedChunks>
<set>
<option value="bundled-js-predefined-d6986cc7102b-b598e85cdad2-JavaScript-PY-252.25557.178" />
<option value="bundled-python-sdk-ce6832f46686-7b97d883f26b-com.jetbrains.pycharm.pro.sharedIndexes.bundled-PY-252.25557.178" />
</set>
</attachedChunks>
</component>
<component name="TaskManager">
<task active="true" id="Default" summary="默认任务">
<changelist id="feebdecb-aab4-468a-89d2-02ab184d6524" name="更改" comment="" />
<created>1760248745025</created>
<option name="number" value="Default" />
<option name="presentableId" value="Default" />
<updated>1760248745025</updated>
<workItem from="1760248747064" duration="34000" />
<workItem from="1760248891758" duration="6962000" />
<workItem from="1760271313858" duration="27000" />
<workItem from="1760271342012" duration="835000" />
<workItem from="1760851293582" duration="1237000" />
<workItem from="1760852569906" duration="605000" />
</task>
<servers />
</component>
<component name="TypeScriptGeneratedFilesManager">
<option name="version" value="3" />
</component>
<component name="Vcs.Log.Tabs.Properties">
<option name="TAB_STATES">
<map>
<entry key="MAIN">
<value>
<State />
</value>
</entry>
</map>
</option>
</component>
</project>

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

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

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

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

@ -18,52 +18,30 @@ class Migration(migrations.Migration):
migrations.CreateModel( migrations.CreateModel(
name='BlogUser', name='BlogUser',
fields=[ fields=[
# xm: 主键ID自增BigAutoField
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
# xm: 密码字段,存储加密后的密码
('password', models.CharField(max_length=128, verbose_name='password')), ('password', models.CharField(max_length=128, verbose_name='password')),
# xm: 最后登录时间,记录用户最后一次登录的时间
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), ('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')), ('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')), ('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')), ('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')), ('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')), ('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')), ('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')), ('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')), ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
# xm: 昵称字段,博客用户特有属性
('nickname', models.CharField(blank=True, max_length=100, verbose_name='昵称')), ('nickname', models.CharField(blank=True, max_length=100, verbose_name='昵称')),
# xm: 创建时间,记录用户账号创建时间
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')), ('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
# xm: 最后修改时间,记录用户信息最后修改时间
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')), ('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
# xm: 创建来源,记录用户注册来源
('source', models.CharField(blank=True, max_length=100, verbose_name='创建来源')), ('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')), ('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')), ('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={ options={
# xm: 单数名称显示
'verbose_name': '用户', 'verbose_name': '用户',
# xm: 复数名称显示
'verbose_name_plural': '用户', 'verbose_name_plural': '用户',
# xm: 默认按ID倒序排列
'ordering': ['-id'], 'ordering': ['-id'],
# xm: 指定最新记录的获取字段
'get_latest_by': 'id', 'get_latest_by': 'id',
}, },
# xm: 指定自定义用户模型的管理器
managers=[ managers=[
('objects', django.contrib.auth.models.UserManager()), ('objects', django.contrib.auth.models.UserManager()),
], ],

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

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

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

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

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

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

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

@ -1,83 +1,47 @@
from django.contrib import admin from django.contrib import admin
from django.urls import reverse from django.urls import reverse
from django.utils.html import format_html from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _ # 国际化翻译 from django.utils.translation import gettext_lazy as _
# gjw:禁用评论状态的动作函数
def disable_commentstatus(modeladmin, request, queryset): def disable_commentstatus(modeladmin, request, queryset):
"""将选中的评论设置为禁用状态"""
queryset.update(is_enable=False) queryset.update(is_enable=False)
#gjw: 启用评论状态的动作函数
def enable_commentstatus(modeladmin, request, queryset): def enable_commentstatus(modeladmin, request, queryset):
"""将选中的评论设置为启用状态"""
queryset.update(is_enable=True) queryset.update(is_enable=True)
# gjw:为动作函数设置显示名称(支持国际化)
disable_commentstatus.short_description = _('Disable comments') disable_commentstatus.short_description = _('Disable comments')
enable_commentstatus.short_description = _('Enable comments') enable_commentstatus.short_description = _('Enable comments')
class CommentAdmin(admin.ModelAdmin): class CommentAdmin(admin.ModelAdmin):
"""评论模型的后台管理配置"""
# gjw:每页显示20条记录
list_per_page = 20 list_per_page = 20
# gjw:列表页显示的字段
list_display = ( list_display = (
'id', # 评论ID 'id',
'body', # 评论内容 'body',
'link_to_userinfo', # 用户信息链接(自定义方法) 'link_to_userinfo',
'link_to_article', # 文章链接(自定义方法) 'link_to_article',
'is_enable', # 是否启用 'is_enable',
'creation_time' # 创建时间 'creation_time')
)
# gjw:可点击进入编辑页面的字段
list_display_links = ('id', 'body', 'is_enable') list_display_links = ('id', 'body', 'is_enable')
list_filter = ('is_enable',)
# gjw:右侧过滤器
list_filter = ('is_enable',) # gjw:按启用状态过滤
# gjw:编辑页面排除的字段(这些字段不会在编辑表单中显示)
exclude = ('creation_time', 'last_modify_time') exclude = ('creation_time', 'last_modify_time')
# gjw:批量动作列表
actions = [disable_commentstatus, enable_commentstatus] actions = [disable_commentstatus, enable_commentstatus]
def link_to_userinfo(self, obj): def link_to_userinfo(self, obj):
"""
生成指向用户详情页的链接
obj: Comment实例
返回包含用户昵称或邮箱的HTML链接
"""
# 获取用户模型的app和model名称
info = (obj.author._meta.app_label, obj.author._meta.model_name) info = (obj.author._meta.app_label, obj.author._meta.model_name)
#gjw: 生成用户编辑页面的URL
link = reverse('admin:%s_%s_change' % info, args=(obj.author.id,)) link = reverse('admin:%s_%s_change' % info, args=(obj.author.id,))
# gjw:返回HTML链接显示用户昵称如果没有则显示邮箱
return format_html( return format_html(
u'<a href="%s">%s</a>' % u'<a href="%s">%s</a>' %
(link, obj.author.nickname if obj.author.nickname else obj.author.email)) (link, obj.author.nickname if obj.author.nickname else obj.author.email))
def link_to_article(self, obj): def link_to_article(self, obj):
"""
生成指向文章详情页的链接
obj: Comment实例
返回包含文章标题的HTML链接
"""
# gjw:获取文章模型的app和model名称
info = (obj.article._meta.app_label, obj.article._meta.model_name) info = (obj.article._meta.app_label, obj.article._meta.model_name)
# 生成文章编辑页面的URL
link = reverse('admin:%s_%s_change' % info, args=(obj.article.id,)) link = reverse('admin:%s_%s_change' % info, args=(obj.article.id,))
# gjw:返回HTML链接显示文章标题
return format_html( return format_html(
u'<a href="%s">%s</a>' % (link, obj.article.title)) u'<a href="%s">%s</a>' % (link, obj.article.title))
# gjw:为自定义方法设置显示名称(支持国际化)
link_to_userinfo.short_description = _('User') link_to_userinfo.short_description = _('User')
link_to_article.short_description = _('Article') link_to_article.short_description = _('Article')

@ -1,24 +1,13 @@
from django import forms from django import forms
from django.forms import ModelForm from django.forms import ModelForm
# gjw:导入Comment模型
from .models import Comment from .models import Comment
class CommentForm(ModelForm): class CommentForm(ModelForm):
"""
评论表单类
用于处理用户提交的评论数据支持回复功能
"""
# gjw:父评论ID字段用于实现评论回复功能
parent_comment_id = forms.IntegerField( parent_comment_id = forms.IntegerField(
widget=forms.HiddenInput, # 使用隐藏输入控件,前端不可见 widget=forms.HiddenInput, required=False)
required=False # 非必填字段,如果是顶级评论则为空
)
class Meta: class Meta:
#gjw: 指定关联的模型
model = Comment model = Comment
# gjw:表单中包含的字段,只包含评论正文
fields = ['body'] fields = ['body']

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

@ -1,24 +1,18 @@
# Generated by Django 4.1.7 on 2023-04-24 13:48 # Generated by Django 4.1.7 on 2023-04-24 13:48
# gjw:导入Django迁移相关模块
from django.db import migrations, models from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
# gjw:迁移类,用于修改数据库结构
# gjw:依赖关系需要先执行comments应用的0001_initial迁移
dependencies = [ dependencies = [
('comments', '0001_initial'), # gjw:依赖本应用的前一个迁移文件 ('comments', '0001_initial'),
] ]
# gjw:迁移操作列表
operations = [ operations = [
# gjw:修改字段操作改变Comment模型的is_enable字段
migrations.AlterField( migrations.AlterField(
model_name='comment', #gjw: 要修改的模型名称 model_name='comment',
name='is_enable', #gjw: 要修改的字段名称 name='is_enable',
# gjw:将字段的默认值从True改为False
# gjw:这意味着新创建的评论默认不会显示,需要手动启用
field=models.BooleanField(default=False, verbose_name='是否显示'), field=models.BooleanField(default=False, verbose_name='是否显示'),
), ),
] ]

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

@ -1,82 +1,39 @@
from django.conf import settings from django.conf import settings
from django.db import models from django.db import models
from django.utils.timezone import now from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _ #gjw: 国际化翻译 from django.utils.translation import gettext_lazy as _
from blog.models import Article # gjw:导入文章模型
from blog.models import Article
# gjw:评论模型
class Comment(models.Model):
"""
评论模型
用于存储用户对文章的评论支持评论回复功能
"""
# gjw:评论正文最大长度300字符 # Create your models here.
body = models.TextField('正文', max_length=300)
# gjw:创建时间,默认值为当前时间 class Comment(models.Model):
body = models.TextField('正文', max_length=300)
creation_time = models.DateTimeField(_('creation time'), default=now) creation_time = models.DateTimeField(_('creation time'), default=now)
# gjw:最后修改时间,默认值为当前时间
last_modify_time = models.DateTimeField(_('last modify time'), default=now) last_modify_time = models.DateTimeField(_('last modify time'), default=now)
# gjw:评论作者,外键关联到用户模型
# gjw:CASCADE: 用户删除时,其所有评论也会被删除
author = models.ForeignKey( author = models.ForeignKey(
settings.AUTH_USER_MODEL, # gjw:使用settings中配置的用户模型 settings.AUTH_USER_MODEL,
verbose_name=_('author'), # gjw:显示名称:作者 verbose_name=_('author'),
on_delete=models.CASCADE # gjw:级联删除 on_delete=models.CASCADE)
)
# gjw:关联的文章外键关联到Article模型
# gjw:CASCADE: 文章删除时,其所有评论也会被删除
article = models.ForeignKey( article = models.ForeignKey(
Article, # gjw:关联到文章模型 Article,
verbose_name=_('article'), # gjw:显示名称:文章 verbose_name=_('article'),
on_delete=models.CASCADE # gjw:级联删除 on_delete=models.CASCADE)
)
# gjw:父级评论,自关联实现评论回复功能
# gjw:blank=True, null=True: 允许为空,表示可以是顶级评论
parent_comment = models.ForeignKey( parent_comment = models.ForeignKey(
'self', # gjw:自关联,指向同一个模型 'self',
verbose_name=_('parent comment'), # gjw:显示名称:父评论 verbose_name=_('parent comment'),
blank=True, # gjw:表单验证允许为空 blank=True,
null=True, # gjw:数据库允许为NULL null=True,
on_delete=models.CASCADE #gjw: 级联删除 on_delete=models.CASCADE)
) is_enable = models.BooleanField(_('enable'),
default=False, blank=False, null=False)
# gjw:是否启用显示
# gjw:default=False: 新评论默认不显示(需要审核)
# gjw:blank=False, null=False: 必填字段,不能为空
is_enable = models.BooleanField(
_('enable'), # 显示名称:启用
default=False, # 默认不显示
blank=False, # 表单验证不允许为空
null=False # 数据库不允许为NULL
)
class Meta: class Meta:
"""模型的元数据配置"""
# gjw:默认按ID降序排列新的评论显示在前面
ordering = ['-id'] ordering = ['-id']
# gjw:单数显示名称
verbose_name = _('comment') verbose_name = _('comment')
# gjw:复数显示名称(与单数相同)
verbose_name_plural = verbose_name verbose_name_plural = verbose_name
# gjw:指定按ID字段获取最新记录
get_latest_by = 'id' get_latest_by = 'id'
def __str__(self): def __str__(self):
"""
模型的字符串表示方法
在Django admin和其他显示场合使用
Returns:
str: 评论正文内容
"""
return self.body return self.body

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

@ -1,21 +1,11 @@
from django.urls import path from django.urls import path
from . import views # gjw:导入当前应用的视图模块 from . import views
# gjw:定义应用命名空间用于URL反向解析时区分不同应用的相同URL名称
app_name = "comments" app_name = "comments"
# gjw:URL模式配置列表
urlpatterns = [ urlpatterns = [
# 评论提交URL配置
path( path(
# gjw:URL模式/article/<文章ID>/postcomment
'article/<int:article_id>/postcomment', 'article/<int:article_id>/postcomment',
# gjw:对应的视图类使用基于类的视图CommentPostView处理请求
views.CommentPostView.as_view(), views.CommentPostView.as_view(),
name='postcomment'),
# gjw:URL名称在模板和视图中使用reverse('comments:postcomment')进行反向解析
name='postcomment'
),
] ]

@ -1,53 +1,28 @@
import logging import logging
from django.utils.translation import gettext_lazy as _ # gjw:国际化翻译
from djangoblog.utils import get_current_site # gjw:获取当前站点信息
from djangoblog.utils import send_email # gjw:邮件发送工具
# gjw:获取当前模块的日志记录器 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__) logger = logging.getLogger(__name__)
def send_comment_email(comment): def send_comment_email(comment):
"""
发送评论相关邮件通知
功能
1. 向评论作者发送感谢邮件
2. 如果评论是回复向被回复者发送通知邮件
Args:
comment: Comment模型实例包含评论相关信息
"""
# gjw:获取当前站点域名
site = get_current_site().domain site = get_current_site().domain
# gjw:邮件主题:感谢评论
subject = _('Thanks for your comment') subject = _('Thanks for your comment')
# gjw:构建文章完整URL
article_url = f"https://{site}{comment.article.get_absolute_url()}" article_url = f"https://{site}{comment.article.get_absolute_url()}"
# gjw:构建感谢评论作者的邮件HTML内容
html_content = _("""<p>Thank you very much for your comments on this site</p> 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> You can visit <a href="%(article_url)s" rel="bookmark">%(article_title)s</a>
to review your comments, to review your comments,
Thank you again! Thank you again!
<br /> <br />
If the link above cannot be opened, please copy this link to your browser. If the link above cannot be opened, please copy this link to your browser.
%(article_url)s""") % { %(article_url)s""") % {'article_url': article_url, 'article_title': comment.article.title}
'article_url': article_url,
'article_title': comment.article.title
}
# gjw:获取评论作者的邮箱
tomail = comment.author.email tomail = comment.author.email
# gjw:发送感谢邮件给评论作者
send_email([tomail], subject, html_content) send_email([tomail], subject, html_content)
try: try:
# gjw:检查是否是回复评论(有父评论)
if comment.parent_comment: if comment.parent_comment:
#gjw: 构建回复通知邮件的HTML内容
html_content = _("""Your comment on <a href="%(article_url)s" rel="bookmark">%(article_title)s</a><br/> has 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 received a reply. <br/> %(comment_body)s
<br/> <br/>
@ -55,18 +30,9 @@ def send_comment_email(comment):
<br/> <br/>
If the link above cannot be opened, please copy this link to your browser. If the link above cannot be opened, please copy this link to your browser.
%(article_url)s %(article_url)s
""") % { """) % {'article_url': article_url, 'article_title': comment.article.title,
'article_url': article_url, 'comment_body': comment.parent_comment.body}
'article_title': comment.article.title,
'comment_body': comment.parent_comment.body # gjw:被回复的评论内容
}
# gjw:获取被回复评论作者的邮箱
tomail = comment.parent_comment.author.email tomail = comment.parent_comment.author.email
# gjw:发送回复通知邮件给被回复者
send_email([tomail], subject, html_content) send_email([tomail], subject, html_content)
except Exception as e: except Exception as e:
# gjw:记录邮件发送异常,但不影响主流程
logger.error(e) logger.error(e)

@ -5,115 +5,59 @@ from django.shortcuts import get_object_or_404
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_protect from django.views.decorators.csrf import csrf_protect
from django.views.generic.edit import FormView from django.views.generic.edit import FormView
from accounts.models import BlogUser from accounts.models import BlogUser
from blog.models import Article from blog.models import Article
from .forms import CommentForm from .forms import CommentForm
from .models import Comment from .models import Comment
class CommentPostView(FormView):
"""
评论提交视图类
处理用户评论的提交和验证
"""
# gjw:指定使用的表单类
class CommentPostView(FormView):
form_class = CommentForm form_class = CommentForm
# gjw:指定模板名称
template_name = 'blog/article_detail.html' template_name = 'blog/article_detail.html'
@method_decorator(csrf_protect) @method_decorator(csrf_protect)
def dispatch(self, *args, **kwargs): def dispatch(self, *args, **kwargs):
"""
重写dispatch方法添加CSRF保护
防止跨站请求伪造攻击
"""
return super(CommentPostView, self).dispatch(*args, **kwargs) return super(CommentPostView, self).dispatch(*args, **kwargs)
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
"""
处理GET请求
当用户通过GET方式访问评论提交URL时重定向到文章详情页的评论区域
"""
# gjw:从URL参数中获取文章ID
article_id = self.kwargs['article_id'] article_id = self.kwargs['article_id']
# gjw:获取文章对象如果不存在返回404
article = get_object_or_404(Article, pk=article_id) article = get_object_or_404(Article, pk=article_id)
# gjw:获取文章的绝对URL
url = article.get_absolute_url() url = article.get_absolute_url()
#gjw: 重定向到文章详情页的评论区域
return HttpResponseRedirect(url + "#comments") return HttpResponseRedirect(url + "#comments")
def form_invalid(self, form): def form_invalid(self, form):
"""
表单验证失败时的处理逻辑
Args:
form: 验证失败的表单对象
Returns:
渲染响应显示错误信息
"""
# gjw:获取文章ID
article_id = self.kwargs['article_id'] article_id = self.kwargs['article_id']
# gjw:获取文章对象
article = get_object_or_404(Article, pk=article_id) article = get_object_or_404(Article, pk=article_id)
# gjw:重新渲染模板,显示表单错误信息
return self.render_to_response({ return self.render_to_response({
'form': form, # gjw:包含错误信息的表单 'form': form,
'article': article #gjw: 文章对象 'article': article
}) })
def form_valid(self, form): def form_valid(self, form):
""" """提交的数据验证合法后的逻辑"""
表单验证成功时的处理逻辑
保存评论数据到数据库
Args:
form: 验证成功的表单对象
Returns:
重定向响应跳转到评论位置
Raises:
ValidationError: 当文章评论关闭时抛出异常
"""
# gjw:获取当前登录用户
user = self.request.user user = self.request.user
# gjw:根据用户ID获取用户对象
author = BlogUser.objects.get(pk=user.pk) author = BlogUser.objects.get(pk=user.pk)
# gjw:从URL参数获取文章ID
article_id = self.kwargs['article_id'] article_id = self.kwargs['article_id']
# gjw:获取文章对象
article = get_object_or_404(Article, pk=article_id) article = get_object_or_404(Article, pk=article_id)
#gjw: 检查文章是否允许评论
#gjw: 'c' 可能表示关闭状态
if article.comment_status == 'c' or article.status == 'c': if article.comment_status == 'c' or article.status == 'c':
raise ValidationError("该文章评论已关闭.") raise ValidationError("该文章评论已关闭.")
# gjw:获取表单数据但不立即保存到数据库
comment = form.save(False) comment = form.save(False)
#gjw: 设置评论关联的文章
comment.article = article comment.article = article
# gjw:获取博客设置
from djangoblog.utils import get_blog_setting from djangoblog.utils import get_blog_setting
settings = get_blog_setting() settings = get_blog_setting()
# gjw:如果博客设置不需要评论审核,则自动启用评论
if not settings.comment_need_review: if not settings.comment_need_review:
comment.is_enable = True comment.is_enable = True
#gjw: 设置评论作者
comment.author = author comment.author = author
#gjw: 处理回复评论的情况
if form.cleaned_data['parent_comment_id']: if form.cleaned_data['parent_comment_id']:
#gjw: 获取父评论对象
parent_comment = Comment.objects.get( parent_comment = Comment.objects.get(
pk=form.cleaned_data['parent_comment_id']) pk=form.cleaned_data['parent_comment_id'])
# gjw:设置评论的父评论
comment.parent_comment = parent_comment comment.parent_comment = parent_comment
# gjw:保存评论到数据库
comment.save(True) comment.save(True)
# gjw:重定向到文章详情页,并定位到新提交的评论位置
return HttpResponseRedirect( return HttpResponseRedirect(
"%s#div-comment-%d" % # gjw:使用锚点定位到具体评论 "%s#div-comment-%d" %
(article.get_absolute_url(), comment.pk)) # gjw:文章URL和评论ID (article.get_absolute_url(), comment.pk))

Loading…
Cancel
Save