diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d860227 Binary files /dev/null and b/.gitignore differ diff --git a/src/djangoblog-master/accounts/admin.py b/src/djangoblog-master/accounts/admin.py index 32e483c..0134c20 100644 --- a/src/djangoblog-master/accounts/admin.py +++ b/src/djangoblog-master/accounts/admin.py @@ -8,14 +8,20 @@ from django.utils.translation import gettext_lazy as _ from .models import BlogUser +# xm: 自定义用户创建表单,继承自ModelForm class BlogUserCreationForm(forms.ModelForm): + # xm: 密码输入字段1 password1 = forms.CharField(label=_('password'), widget=forms.PasswordInput) + # xm: 密码确认字段2 password2 = forms.CharField(label=_('Enter password again'), widget=forms.PasswordInput) class Meta: + # xm: 指定关联的模型为BlogUser model = BlogUser + # xm: 表单字段只包含email fields = ('email',) + # xm: 密码验证方法,确保两次输入的密码一致 def clean_password2(self): # Check that the two password entries match password1 = self.cleaned_data.get("password1") @@ -24,29 +30,40 @@ class BlogUserCreationForm(forms.ModelForm): raise forms.ValidationError(_("passwords do not match")) return password2 + # xm: 保存用户信息,对密码进行哈希处理 def save(self, commit=True): # Save the provided password in hashed format user = super().save(commit=False) user.set_password(self.cleaned_data["password1"]) if commit: + # xm: 设置用户来源为管理员站点 user.source = 'adminsite' user.save() return user +# xm: 自定义用户信息修改表单,继承自UserChangeForm class BlogUserChangeForm(UserChangeForm): class Meta: + # xm: 指定关联的模型为BlogUser model = BlogUser + # xm: 包含所有字段 fields = '__all__' + # xm: 指定username字段使用UsernameField类型 field_classes = {'username': UsernameField} + # xm: 初始化方法 def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) +# xm: 自定义用户管理类,继承自UserAdmin class BlogUserAdmin(UserAdmin): + # xm: 指定修改表单类 form = BlogUserChangeForm + # xm: 指定创建表单类 add_form = BlogUserCreationForm + # xm: 列表页面显示的字段 list_display = ( 'id', 'nickname', @@ -55,5 +72,7 @@ class BlogUserAdmin(UserAdmin): 'last_login', 'date_joined', 'source') + # xm: 列表页面可点击链接的字段 list_display_links = ('id', 'username') + # xm: 默认排序字段,按ID倒序 ordering = ('-id',) diff --git a/src/djangoblog-master/accounts/apps.py b/src/djangoblog-master/accounts/apps.py index 9b3fc5a..39966b1 100644 --- a/src/djangoblog-master/accounts/apps.py +++ b/src/djangoblog-master/accounts/apps.py @@ -1,5 +1,5 @@ from django.apps import AppConfig - - +# xm: 应用配置类,继承自Django的AppConfig基类 class AccountsConfig(AppConfig): + # xm: 指定应用的完整Python路径 name = 'accounts' diff --git a/src/djangoblog-master/accounts/forms.py b/src/djangoblog-master/accounts/forms.py index fce4137..eca8070 100644 --- a/src/djangoblog-master/accounts/forms.py +++ b/src/djangoblog-master/accounts/forms.py @@ -8,28 +8,37 @@ from . import utils from .models import BlogUser +# xm: 自定义登录表单,继承自Django的AuthenticationForm class LoginForm(AuthenticationForm): def __init__(self, *args, **kwargs): super(LoginForm, self).__init__(*args, **kwargs) + # xm: 设置用户名字段的widget,添加placeholder和CSS类 self.fields['username'].widget = widgets.TextInput( attrs={'placeholder': "username", "class": "form-control"}) + # xm: 设置密码字段的widget,添加placeholder和CSS类 self.fields['password'].widget = widgets.PasswordInput( attrs={'placeholder': "password", "class": "form-control"}) +# xm: 自定义用户注册表单,继承自Django的UserCreationForm class RegisterForm(UserCreationForm): def __init__(self, *args, **kwargs): super(RegisterForm, self).__init__(*args, **kwargs) + # xm: 设置用户名字段的widget,添加placeholder和CSS类 self.fields['username'].widget = widgets.TextInput( attrs={'placeholder': "username", "class": "form-control"}) + # xm: 设置邮箱字段的widget,添加placeholder和CSS类 self.fields['email'].widget = widgets.EmailInput( attrs={'placeholder': "email", "class": "form-control"}) + # xm: 设置密码字段的widget,添加placeholder和CSS类 self.fields['password1'].widget = widgets.PasswordInput( attrs={'placeholder': "password", "class": "form-control"}) + # xm: 设置密码确认字段的widget,添加placeholder和CSS类 self.fields['password2'].widget = widgets.PasswordInput( attrs={'placeholder': "repeat password", "class": "form-control"}) + # xm: 邮箱验证方法,确保邮箱唯一性 def clean_email(self): email = self.cleaned_data['email'] if get_user_model().objects.filter(email=email).exists(): @@ -37,11 +46,15 @@ class RegisterForm(UserCreationForm): return email class Meta: + # xm: 指定关联的用户模型 model = get_user_model() + # xm: 表单包含的字段:用户名和邮箱 fields = ("username", "email") +# xm: 忘记密码表单,继承自forms.Form class ForgetPasswordForm(forms.Form): + # xm: 新密码输入字段 new_password1 = forms.CharField( label=_("New password"), widget=forms.PasswordInput( @@ -52,6 +65,7 @@ class ForgetPasswordForm(forms.Form): ), ) + # xm: 确认新密码字段 new_password2 = forms.CharField( label="确认密码", widget=forms.PasswordInput( @@ -62,6 +76,7 @@ class ForgetPasswordForm(forms.Form): ), ) + # xm: 邮箱输入字段,用于验证用户身份 email = forms.EmailField( label='邮箱', widget=forms.TextInput( @@ -72,6 +87,7 @@ class ForgetPasswordForm(forms.Form): ), ) + # xm: 验证码输入字段 code = forms.CharField( label=_('Code'), widget=forms.TextInput( @@ -82,15 +98,17 @@ class ForgetPasswordForm(forms.Form): ), ) + # xm: 验证两次输入的新密码是否一致 def clean_new_password2(self): password1 = self.data.get("new_password1") password2 = self.data.get("new_password2") if password1 and password2 and password1 != password2: raise ValidationError(_("passwords do not match")) + # xm: 使用Django的密码验证器验证密码强度 password_validation.validate_password(password2) - return password2 + # xm: 验证邮箱是否存在系统中 def clean_email(self): user_email = self.cleaned_data.get("email") if not BlogUser.objects.filter( @@ -100,6 +118,7 @@ class ForgetPasswordForm(forms.Form): raise ValidationError(_("email does not exist")) return user_email + # xm: 验证验证码是否正确 def clean_code(self): code = self.cleaned_data.get("code") error = utils.verify( @@ -111,7 +130,9 @@ class ForgetPasswordForm(forms.Form): return code +# xm: 获取忘记密码验证码的表单 class ForgetPasswordCodeForm(forms.Form): + # xm: 邮箱字段,用于发送验证码 email = forms.EmailField( label=_('Email'), ) diff --git a/src/djangoblog-master/accounts/migrations/0001_initial.py b/src/djangoblog-master/accounts/migrations/0001_initial.py index d2fbcab..75c2b85 100644 --- a/src/djangoblog-master/accounts/migrations/0001_initial.py +++ b/src/djangoblog-master/accounts/migrations/0001_initial.py @@ -18,30 +18,52 @@ class Migration(migrations.Migration): migrations.CreateModel( name='BlogUser', fields=[ + # xm: 主键ID,自增BigAutoField ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + # xm: 密码字段,存储加密后的密码 ('password', models.CharField(max_length=128, verbose_name='password')), + # xm: 最后登录时间,记录用户最后一次登录的时间 ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), + # xm: 超级用户标志,标记用户是否拥有所有权限 ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), + # xm: 用户名,唯一且需要符合验证器规则 ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), + # xm: 名字字段,可选 ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')), + # xm: 姓氏字段,可选 ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), + # xm: 邮箱地址,可选 ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), + # xm: 职员状态,标记用户是否可以访问管理后台 ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), + # xm: 活跃状态,标记用户账号是否激活 ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), + # xm: 加入日期,记录用户注册时间 ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), + # xm: 昵称字段,博客用户特有属性 ('nickname', models.CharField(blank=True, max_length=100, verbose_name='昵称')), + # xm: 创建时间,记录用户账号创建时间 ('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')), + # xm: 最后修改时间,记录用户信息最后修改时间 ('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')), + # xm: 创建来源,记录用户注册来源 ('source', models.CharField(blank=True, max_length=100, verbose_name='创建来源')), + # xm: 用户组多对多关系 ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')), + # xm: 用户权限多对多关系 ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')), ], options={ + # xm: 单数名称显示 'verbose_name': '用户', + # xm: 复数名称显示 'verbose_name_plural': '用户', + # xm: 默认按ID倒序排列 'ordering': ['-id'], + # xm: 指定最新记录的获取字段 'get_latest_by': 'id', }, + # xm: 指定自定义用户模型的管理器 managers=[ ('objects', django.contrib.auth.models.UserManager()), ], diff --git a/src/djangoblog-master/accounts/migrations/0002_alter_bloguser_options_remove_bloguser_created_time_and_more.py b/src/djangoblog-master/accounts/migrations/0002_alter_bloguser_options_remove_bloguser_created_time_and_more.py index 1a9f509..8f9221c 100644 --- a/src/djangoblog-master/accounts/migrations/0002_alter_bloguser_options_remove_bloguser_created_time_and_more.py +++ b/src/djangoblog-master/accounts/migrations/0002_alter_bloguser_options_remove_bloguser_created_time_and_more.py @@ -11,33 +11,40 @@ class Migration(migrations.Migration): ] operations = [ + # xm: 修改BlogUser模型的元数据选项 migrations.AlterModelOptions( name='bloguser', options={'get_latest_by': 'id', 'ordering': ['-id'], 'verbose_name': 'user', 'verbose_name_plural': 'user'}, ), + # xm: 删除旧的created_time字段 migrations.RemoveField( model_name='bloguser', name='created_time', ), + # xm: 删除旧的last_mod_time字段 migrations.RemoveField( model_name='bloguser', name='last_mod_time', ), + # xm: 添加新的creation_time字段,使用当前时间作为默认值 migrations.AddField( model_name='bloguser', name='creation_time', field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'), ), + # xm: 添加新的last_modify_time字段,使用当前时间作为默认值 migrations.AddField( model_name='bloguser', name='last_modify_time', field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='last modify time'), ), + # xm: 修改nickname字段的verbose_name为英文 migrations.AlterField( model_name='bloguser', name='nickname', field=models.CharField(blank=True, max_length=100, verbose_name='nick name'), ), + # xm: 修改source字段的verbose_name为英文 migrations.AlterField( model_name='bloguser', name='source', diff --git a/src/djangoblog-master/accounts/models.py b/src/djangoblog-master/accounts/models.py index 3baddbb..844150a 100644 --- a/src/djangoblog-master/accounts/models.py +++ b/src/djangoblog-master/accounts/models.py @@ -8,20 +8,28 @@ from djangoblog.utils import get_current_site # Create your models here. +# xm: 自定义用户模型,继承自Django的AbstractUser基类 class BlogUser(AbstractUser): + # xm: 昵称字段,最大长度100,可为空 nickname = models.CharField(_('nick name'), max_length=100, blank=True) + # xm: 创建时间字段,默认值为当前时间 creation_time = models.DateTimeField(_('creation time'), default=now) + # xm: 最后修改时间字段,默认值为当前时间 last_modify_time = models.DateTimeField(_('last modify time'), default=now) + # xm: 用户来源字段,记录创建来源,可为空 source = models.CharField(_('create source'), max_length=100, blank=True) + # xm: 获取用户绝对URL的方法,用于生成作者详情页链接 def get_absolute_url(self): return reverse( 'blog:author_detail', kwargs={ 'author_name': self.username}) + # xm: 对象的字符串表示形式,返回邮箱地址 def __str__(self): return self.email + # xm: 获取用户完整URL的方法,包含域名 def get_full_url(self): site = get_current_site().domain url = "https://{site}{path}".format(site=site, @@ -29,7 +37,11 @@ class BlogUser(AbstractUser): return url class Meta: + # xm: 默认按ID倒序排列 ordering = ['-id'] + # xm: 单数名称显示 verbose_name = _('user') + # xm: 复数名称显示(与单数相同) verbose_name_plural = verbose_name + # xm: 指定最新记录的获取字段 get_latest_by = 'id' diff --git a/src/djangoblog-master/accounts/tests.py b/src/djangoblog-master/accounts/tests.py index 6893411..a1dcbf9 100644 --- a/src/djangoblog-master/accounts/tests.py +++ b/src/djangoblog-master/accounts/tests.py @@ -11,10 +11,13 @@ from . import utils # Create your tests here. +# xm: 账户功能测试类,继承自Django的TestCase class AccountTest(TestCase): + # xm: 测试初始化方法,每个测试方法执行前都会运行 def setUp(self): self.client = Client() self.factory = RequestFactory() + # xm: 创建测试用户 self.blog_user = BlogUser.objects.create_user( username="test", email="admin@admin.com", @@ -22,27 +25,33 @@ class AccountTest(TestCase): ) self.new_test = "xxx123--=" + # xm: 测试账户验证功能 def test_validate_account(self): site = get_current_site().domain + # xm: 创建超级用户 user = BlogUser.objects.create_superuser( email="liangliangyy1@gmail.com", username="liangliangyy1", password="qwer!@#$ggg") testuser = BlogUser.objects.get(username='liangliangyy1') + # xm: 测试用户登录功能 loginresult = self.client.login( username='liangliangyy1', password='qwer!@#$ggg') self.assertEqual(loginresult, True) + # xm: 测试管理员页面访问权限 response = self.client.get('/admin/') self.assertEqual(response.status_code, 200) + # xm: 创建测试分类 category = Category() category.name = "categoryaaa" category.creation_time = timezone.now() category.last_modify_time = timezone.now() category.save() + # xm: 创建测试文章 article = Article() article.title = "nicetitleaaa" article.body = "nicecontentaaa" @@ -52,44 +61,53 @@ class AccountTest(TestCase): article.status = 'p' article.save() + # xm: 测试文章管理页面访问权限 response = self.client.get(article.get_admin_url()) self.assertEqual(response.status_code, 200) + # xm: 测试用户注册功能 def test_validate_register(self): self.assertEquals( 0, len( BlogUser.objects.filter( email='user123@user.com'))) + # xm: 模拟用户注册请求 response = self.client.post(reverse('account:register'), { 'username': 'user1233', 'email': 'user123@user.com', 'password1': 'password123!q@wE#R$T', 'password2': 'password123!q@wE#R$T', }) + # xm: 验证用户是否创建成功 self.assertEquals( 1, len( BlogUser.objects.filter( email='user123@user.com'))) user = BlogUser.objects.filter(email='user123@user.com')[0] + # xm: 生成验证签名 sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id))) path = reverse('accounts:result') url = '{path}?type=validation&id={id}&sign={sign}'.format( path=path, id=user.id, sign=sign) + # xm: 测试验证页面访问 response = self.client.get(url) self.assertEqual(response.status_code, 200) + # xm: 测试用户登录功能 self.client.login(username='user1233', password='password123!q@wE#R$T') user = BlogUser.objects.filter(email='user123@user.com')[0] user.is_superuser = True user.is_staff = True user.save() delete_sidebar_cache() + # xm: 创建测试分类 category = Category() category.name = "categoryaaa" category.creation_time = timezone.now() category.last_modify_time = timezone.now() category.save() + # xm: 创建测试文章 article = Article() article.category = category article.title = "nicetitle333" @@ -100,36 +118,45 @@ class AccountTest(TestCase): article.status = 'p' article.save() + # xm: 测试文章管理页面访问权限 response = self.client.get(article.get_admin_url()) self.assertEqual(response.status_code, 200) + # xm: 测试用户登出功能 response = self.client.get(reverse('account:logout')) self.assertIn(response.status_code, [301, 302, 200]) + # xm: 测试登出后访问文章管理页面的重定向 response = self.client.get(article.get_admin_url()) self.assertIn(response.status_code, [301, 302, 200]) + # xm: 测试错误密码登录 response = self.client.post(reverse('account:login'), { 'username': 'user1233', 'password': 'password123' }) self.assertIn(response.status_code, [301, 302, 200]) + # xm: 测试登录后访问文章管理页面 response = self.client.get(article.get_admin_url()) self.assertIn(response.status_code, [301, 302, 200]) + # xm: 测试邮箱验证码功能 def test_verify_email_code(self): to_email = "admin@admin.com" code = generate_code() utils.set_code(to_email, code) utils.send_verify_email(to_email, code) + # xm: 测试验证码验证成功情况 err = utils.verify("admin@admin.com", code) self.assertEqual(err, None) + # xm: 测试验证码验证失败情况 err = utils.verify("admin@123.com", code) self.assertEqual(type(err), str) + # xm: 测试忘记密码验证码发送成功情况 def test_forget_password_email_code_success(self): resp = self.client.post( path=reverse("account:forget_password_code"), @@ -139,6 +166,7 @@ class AccountTest(TestCase): self.assertEqual(resp.status_code, 200) self.assertEqual(resp.content.decode("utf-8"), "ok") + # xm: 测试忘记密码验证码发送失败情况 def test_forget_password_email_code_fail(self): resp = self.client.post( path=reverse("account:forget_password_code"), @@ -152,6 +180,7 @@ class AccountTest(TestCase): ) self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱") + # xm: 测试忘记密码重置成功情况 def test_forget_password_email_success(self): code = generate_code() utils.set_code(self.blog_user.email, code) @@ -167,13 +196,14 @@ class AccountTest(TestCase): ) self.assertEqual(resp.status_code, 302) - # 验证用户密码是否修改成功 + # xm: 验证用户密码是否修改成功 blog_user = BlogUser.objects.filter( email=self.blog_user.email, ).first() # type: BlogUser self.assertNotEqual(blog_user, None) self.assertEqual(blog_user.check_password(data["new_password1"]), True) + # xm: 测试不存在的用户忘记密码情况 def test_forget_password_email_not_user(self): data = dict( new_password1=self.new_test, @@ -188,7 +218,7 @@ class AccountTest(TestCase): self.assertEqual(resp.status_code, 200) - + # xm: 测试验证码错误的忘记密码情况 def test_forget_password_email_code_error(self): code = generate_code() utils.set_code(self.blog_user.email, code) diff --git a/src/djangoblog-master/accounts/urls.py b/src/djangoblog-master/accounts/urls.py index 107a801..1fc8c80 100644 --- a/src/djangoblog-master/accounts/urls.py +++ b/src/djangoblog-master/accounts/urls.py @@ -4,25 +4,39 @@ from django.urls import re_path from . import views from .forms import LoginForm +# xm: 定义应用命名空间为"accounts" 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'), - ] +# xm: 定义URL路由配置列表 +urlpatterns = [ + # xm: 登录路由,使用LoginView视图类,指定成功跳转URL和自定义登录表单 + re_path(r'^login/$', + views.LoginView.as_view(success_url='/'), + name='login', + kwargs={'authentication_form': LoginForm}), + + # xm: 注册路由,使用RegisterView视图类,指定成功跳转URL + re_path(r'^register/$', + views.RegisterView.as_view(success_url="/"), + name='register'), + + # xm: 登出路由,使用LogoutView视图类 + re_path(r'^logout/$', + views.LogoutView.as_view(), + name='logout'), + + # xm: 账户结果页面路由,使用函数视图account_result + path(r'account/result.html', + views.account_result, + name='result'), + + # xm: 忘记密码路由,使用ForgetPasswordView视图类 + re_path(r'^forget_password/$', + views.ForgetPasswordView.as_view(), + name='forget_password'), + + # xm: 忘记密码验证码路由,使用ForgetPasswordEmailCode视图类 + re_path(r'^forget_password_code/$', + views.ForgetPasswordEmailCode.as_view(), + name='forget_password_code'), +] diff --git a/src/djangoblog-master/accounts/user_login_backend.py b/src/djangoblog-master/accounts/user_login_backend.py index 73cdca1..c1ad078 100644 --- a/src/djangoblog-master/accounts/user_login_backend.py +++ b/src/djangoblog-master/accounts/user_login_backend.py @@ -2,25 +2,34 @@ from django.contrib.auth import get_user_model from django.contrib.auth.backends import ModelBackend +# xm: 自定义认证后端,允许使用用户名或邮箱登录,继承自ModelBackend class EmailOrUsernameModelBackend(ModelBackend): """ 允许使用用户名或邮箱登录 """ + # xm: 用户认证方法,重写父类方法 def authenticate(self, request, username=None, password=None, **kwargs): + # xm: 判断输入是否包含@符号,决定使用邮箱还是用户名查询 if '@' in username: kwargs = {'email': username} else: kwargs = {'username': username} try: + # xm: 根据用户名或邮箱查询用户 user = get_user_model().objects.get(**kwargs) + # xm: 验证密码是否正确 if user.check_password(password): return user + # xm: 捕获用户不存在的异常 except get_user_model().DoesNotExist: return None + # xm: 根据用户ID获取用户对象的方法 def get_user(self, username): try: + # xm: 通过主键查询用户 return get_user_model().objects.get(pk=username) + # xm: 捕获用户不存在的异常 except get_user_model().DoesNotExist: return None diff --git a/src/djangoblog-master/accounts/utils.py b/src/djangoblog-master/accounts/utils.py index 4b94bdf..0657d0e 100644 --- a/src/djangoblog-master/accounts/utils.py +++ b/src/djangoblog-master/accounts/utils.py @@ -10,6 +10,7 @@ from djangoblog.utils import send_email _code_ttl = timedelta(minutes=5) +# xm: 发送验证邮件函数 def send_verify_email(to_mail: str, code: str, subject: str = _("Verify Email")): """发送重设密码验证码 Args: @@ -17,12 +18,15 @@ def send_verify_email(to_mail: str, code: str, subject: str = _("Verify Email")) subject: 邮件主题 code: 验证码 """ + # xm: 构建邮件HTML内容,包含验证码信息 html_content = _( "You are resetting the password, the verification code is:%(code)s, valid within 5 minutes, please keep it " "properly") % {'code': code} + # xm: 调用发送邮件函数发送验证码 send_email([to_mail], subject, html_content) +# xm: 验证验证码函数 def verify(email: str, code: str) -> typing.Optional[str]: """验证code是否有效 Args: @@ -34,16 +38,22 @@ def verify(email: str, code: str) -> typing.Optional[str]: 这里的错误处理不太合理,应该采用raise抛出 否测调用方也需要对error进行处理 """ + # xm: 从缓存中获取对应邮箱的验证码 cache_code = get_code(email) + # xm: 比较输入的验证码和缓存中的验证码是否一致 if cache_code != code: return gettext("Verification code error") +# xm: 设置验证码到缓存函数 def set_code(email: str, code: str): """设置code""" + # xm: 使用Django缓存系统存储验证码,设置过期时间 cache.set(email, code, _code_ttl.seconds) +# xm: 从缓存获取验证码函数 def get_code(email: str) -> typing.Optional[str]: """获取code""" + # xm: 从Django缓存系统中获取指定邮箱的验证码 return cache.get(email) diff --git a/src/djangoblog-master/accounts/views.py b/src/djangoblog-master/accounts/views.py index ae67aec..cd40dd2 100644 --- a/src/djangoblog-master/accounts/views.py +++ b/src/djangoblog-master/accounts/views.py @@ -30,22 +30,26 @@ logger = logging.getLogger(__name__) # Create your views here. - +# xm: 用户注册视图类,继承自FormView class RegisterView(FormView): form_class = RegisterForm template_name = 'account/registration_form.html' + # xm: 添加CSRF保护装饰器 @method_decorator(csrf_protect) def dispatch(self, *args, **kwargs): return super(RegisterView, self).dispatch(*args, **kwargs) + # xm: 表单验证通过后的处理逻辑 def form_valid(self, form): if form.is_valid(): + # xm: 保存用户但不提交到数据库 user = form.save(False) user.is_active = False user.source = 'Register' user.save(True) site = get_current_site().domain + # xm: 生成邮箱验证签名 sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id))) if settings.DEBUG: @@ -54,6 +58,7 @@ class RegisterView(FormView): url = "http://{site}{path}?type=validation&id={id}&sign={sign}".format( site=site, path=path, id=user.id, sign=sign) + # xm: 构建验证邮件内容 content = """
请点击下面链接验证您的邮箱
@@ -64,6 +69,7 @@ class RegisterView(FormView): 如果上面链接无法打开,请将此链接复制至浏览器。 {url} """.format(url=url) + # xm: 发送验证邮件 send_email( emailto=[ user.email, @@ -71,6 +77,7 @@ class RegisterView(FormView): title='验证您的电子邮箱', content=content) + # xm: 重定向到结果页面 url = reverse('accounts:result') + \ '?type=register&id=' + str(user.id) return HttpResponseRedirect(url) @@ -80,19 +87,24 @@ class RegisterView(FormView): }) +# xm: 用户登出视图类,继承自RedirectView class LogoutView(RedirectView): url = '/login/' + # xm: 添加不缓存装饰器 @method_decorator(never_cache) def dispatch(self, request, *args, **kwargs): return super(LogoutView, self).dispatch(request, *args, **kwargs) + # xm: 处理GET请求,执行登出操作 def get(self, request, *args, **kwargs): logout(request) + # xm: 删除侧边栏缓存 delete_sidebar_cache() return super(LogoutView, self).get(request, *args, **kwargs) +# xm: 用户登录视图类,继承自FormView class LoginView(FormView): form_class = LoginForm template_name = 'account/login.html' @@ -100,41 +112,45 @@ class LoginView(FormView): redirect_field_name = REDIRECT_FIELD_NAME login_ttl = 2626560 # 一个月的时间 + # xm: 添加多个安全相关的装饰器 @method_decorator(sensitive_post_parameters('password')) @method_decorator(csrf_protect) @method_decorator(never_cache) def dispatch(self, request, *args, **kwargs): - return super(LoginView, self).dispatch(request, *args, **kwargs) + # xm: 获取上下文数据,处理重定向参数 def get_context_data(self, **kwargs): redirect_to = self.request.GET.get(self.redirect_field_name) if redirect_to is None: redirect_to = '/' kwargs['redirect_to'] = redirect_to - return super(LoginView, self).get_context_data(**kwargs) + # xm: 表单验证通过后的处理逻辑 def form_valid(self, form): form = AuthenticationForm(data=self.request.POST, request=self.request) if form.is_valid(): + # xm: 删除侧边栏缓存 delete_sidebar_cache() logger.info(self.redirect_field_name) + # xm: 执行用户登录 auth.login(self.request, form.get_user()) + # xm: 处理"记住我"功能,设置会话过期时间 if self.request.POST.get("remember"): self.request.session.set_expiry(self.login_ttl) return super(LoginView, self).form_valid(form) - # return HttpResponseRedirect('/') else: return self.render_to_response({ 'form': form }) + # xm: 获取登录成功后的重定向URL def get_success_url(self): - redirect_to = self.request.POST.get(self.redirect_field_name) + # xm: 验证重定向URL的安全性 if not url_has_allowed_host_and_scheme( url=redirect_to, allowed_hosts=[ self.request.get_host()]): @@ -142,25 +158,30 @@ class LoginView(FormView): return redirect_to +# xm: 账户操作结果页面视图函数 def account_result(request): type = request.GET.get('type') id = request.GET.get('id') + # xm: 获取用户对象,不存在则返回404 user = get_object_or_404(get_user_model(), id=id) logger.info(type) if user.is_active: return HttpResponseRedirect('/') if type and type in ['register', 'validation']: if type == 'register': + # xm: 注册成功页面内容 content = ''' 恭喜您注册成功,一封验证邮件已经发送到您的邮箱,请验证您的邮箱后登录本站。 ''' title = '注册成功' else: + # xm: 验证邮箱签名 c_sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id))) sign = request.GET.get('sign') if sign != c_sign: return HttpResponseForbidden() + # xm: 激活用户账户 user.is_active = True user.save() content = ''' @@ -175,12 +196,15 @@ def account_result(request): return HttpResponseRedirect('/') +# xm: 忘记密码视图类,继承自FormView class ForgetPasswordView(FormView): form_class = ForgetPasswordForm template_name = 'account/forget_password.html' + # xm: 表单验证通过后的处理逻辑 def form_valid(self, form): if form.is_valid(): + # xm: 根据邮箱获取用户并重置密码 blog_user = BlogUser.objects.filter(email=form.cleaned_data.get("email")).get() blog_user.password = make_password(form.cleaned_data["new_password2"]) blog_user.save() @@ -189,14 +213,17 @@ class ForgetPasswordView(FormView): return self.render_to_response({'form': form}) +# xm: 忘记密码验证码发送视图类,继承自View class ForgetPasswordEmailCode(View): + # xm: 处理POST请求,发送验证码邮件 def post(self, request: HttpRequest): form = ForgetPasswordCodeForm(request.POST) if not form.is_valid(): return HttpResponse("错误的邮箱") to_email = form.cleaned_data["email"] + # xm: 生成并发送验证码 code = generate_code() utils.send_verify_email(to_email, code) utils.set_code(to_email, code) diff --git a/src/djangoblog-master/comments/admin.py b/src/djangoblog-master/comments/admin.py index a814f3f..71cffd4 100644 --- a/src/djangoblog-master/comments/admin.py +++ b/src/djangoblog-master/comments/admin.py @@ -1,47 +1,83 @@ 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 _ +from django.utils.translation import gettext_lazy as _ # 国际化翻译 +# gjw:禁用评论状态的动作函数 def disable_commentstatus(modeladmin, request, queryset): + """将选中的评论设置为禁用状态""" queryset.update(is_enable=False) +#gjw: 启用评论状态的动作函数 def enable_commentstatus(modeladmin, request, queryset): + """将选中的评论设置为启用状态""" queryset.update(is_enable=True) +# gjw:为动作函数设置显示名称(支持国际化) disable_commentstatus.short_description = _('Disable comments') enable_commentstatus.short_description = _('Enable comments') class CommentAdmin(admin.ModelAdmin): + """评论模型的后台管理配置""" + + # gjw:每页显示20条记录 list_per_page = 20 + + # gjw:列表页显示的字段 list_display = ( - 'id', - 'body', - 'link_to_userinfo', - 'link_to_article', - 'is_enable', - 'creation_time') + 'id', # 评论ID + 'body', # 评论内容 + 'link_to_userinfo', # 用户信息链接(自定义方法) + 'link_to_article', # 文章链接(自定义方法) + 'is_enable', # 是否启用 + 'creation_time' # 创建时间 + ) + + # gjw:可点击进入编辑页面的字段 list_display_links = ('id', 'body', 'is_enable') - list_filter = ('is_enable',) + + # gjw:右侧过滤器 + list_filter = ('is_enable',) # gjw:按启用状态过滤 + + # gjw:编辑页面排除的字段(这些字段不会在编辑表单中显示) exclude = ('creation_time', 'last_modify_time') + + # gjw:批量动作列表 actions = [disable_commentstatus, enable_commentstatus] def link_to_userinfo(self, obj): + """ + 生成指向用户详情页的链接 + obj: Comment实例 + 返回:包含用户昵称或邮箱的HTML链接 + """ + # 获取用户模型的app和model名称 info = (obj.author._meta.app_label, obj.author._meta.model_name) + #gjw: 生成用户编辑页面的URL link = reverse('admin:%s_%s_change' % info, args=(obj.author.id,)) + # gjw:返回HTML链接,显示用户昵称(如果没有则显示邮箱) return format_html( u'%s' % (link, obj.author.nickname if obj.author.nickname else obj.author.email)) def link_to_article(self, obj): + """ + 生成指向文章详情页的链接 + obj: Comment实例 + 返回:包含文章标题的HTML链接 + """ + # gjw:获取文章模型的app和model名称 info = (obj.article._meta.app_label, obj.article._meta.model_name) + # 生成文章编辑页面的URL link = reverse('admin:%s_%s_change' % info, args=(obj.article.id,)) + # gjw:返回HTML链接,显示文章标题 return format_html( u'%s' % (link, obj.article.title)) + # gjw:为自定义方法设置显示名称(支持国际化) link_to_userinfo.short_description = _('User') - link_to_article.short_description = _('Article') + link_to_article.short_description = _('Article') \ No newline at end of file diff --git a/src/djangoblog-master/comments/forms.py b/src/djangoblog-master/comments/forms.py index e83737d..af47829 100644 --- a/src/djangoblog-master/comments/forms.py +++ b/src/djangoblog-master/comments/forms.py @@ -1,13 +1,24 @@ from django import forms from django.forms import ModelForm +# gjw:导入Comment模型 from .models import Comment class CommentForm(ModelForm): + """ + 评论表单类 + 用于处理用户提交的评论数据,支持回复功能 + """ + + # gjw:父评论ID字段,用于实现评论回复功能 parent_comment_id = forms.IntegerField( - widget=forms.HiddenInput, required=False) + widget=forms.HiddenInput, # 使用隐藏输入控件,前端不可见 + required=False # 非必填字段,如果是顶级评论则为空 + ) class Meta: + #gjw: 指定关联的模型 model = Comment - fields = ['body'] + # gjw:表单中包含的字段,只包含评论正文 + fields = ['body'] \ No newline at end of file diff --git a/src/djangoblog-master/comments/migrations/0001_initial.py b/src/djangoblog-master/comments/migrations/0001_initial.py index 61d1e53..1567928 100644 --- a/src/djangoblog-master/comments/migrations/0001_initial.py +++ b/src/djangoblog-master/comments/migrations/0001_initial.py @@ -1,38 +1,49 @@ # 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'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('blog', '0001_initial'), #gjw:依赖blog应用的初始迁移 + migrations.swappable_dependency(settings.AUTH_USER_MODEL), #gjw:依赖可切换的用户模型 ] + #gjw:迁移操作列表 operations = [ + #gjw:创建Comment模型对应的数据库表 migrations.CreateModel( - name='Comment', + name='Comment', #gjw:模型名称:评论 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': '评论', - 'verbose_name_plural': '评论', - 'ordering': ['-id'], - 'get_latest_by': 'id', + 'verbose_name': '评论', #gjw:单数名称 + 'verbose_name_plural': '评论', #gjw:复数名称 + 'ordering': ['-id'], #gjw:默认按ID降序排列 + 'get_latest_by': 'id', #gjw:指定按ID字段获取最新记录 }, ), - ] + ] \ No newline at end of file diff --git a/src/djangoblog-master/comments/migrations/0002_alter_comment_is_enable.py b/src/djangoblog-master/comments/migrations/0002_alter_comment_is_enable.py index 17c44db..5c8866e 100644 --- a/src/djangoblog-master/comments/migrations/0002_alter_comment_is_enable.py +++ b/src/djangoblog-master/comments/migrations/0002_alter_comment_is_enable.py @@ -1,18 +1,24 @@ # Generated by Django 4.1.7 on 2023-04-24 13:48 - +# gjw:导入Django迁移相关模块 from django.db import migrations, models class Migration(migrations.Migration): + # gjw:迁移类,用于修改数据库结构 + # gjw:依赖关系:需要先执行comments应用的0001_initial迁移 dependencies = [ - ('comments', '0001_initial'), + ('comments', '0001_initial'), # gjw:依赖本应用的前一个迁移文件 ] + # gjw:迁移操作列表 operations = [ + # gjw:修改字段操作:改变Comment模型的is_enable字段 migrations.AlterField( - model_name='comment', - name='is_enable', + model_name='comment', #gjw: 要修改的模型名称 + name='is_enable', #gjw: 要修改的字段名称 + # gjw:将字段的默认值从True改为False + # gjw:这意味着新创建的评论默认不会显示,需要手动启用 field=models.BooleanField(default=False, verbose_name='是否显示'), ), - ] + ] \ No newline at end of file diff --git a/src/djangoblog-master/comments/migrations/0003_alter_comment_options_remove_comment_created_time_and_more.py b/src/djangoblog-master/comments/migrations/0003_alter_comment_options_remove_comment_created_time_and_more.py index a1ca970..19379b2 100644 --- a/src/djangoblog-master/comments/migrations/0003_alter_comment_options_remove_comment_created_time_and_more.py +++ b/src/djangoblog-master/comments/migrations/0003_alter_comment_options_remove_comment_created_time_and_more.py @@ -1,5 +1,6 @@ # 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 @@ -7,54 +8,80 @@ 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', - options={'get_latest_by': 'id', 'ordering': ['-id'], 'verbose_name': 'comment', 'verbose_name_plural': 'comment'}, + name='comment', # gjw:模型名称 + options={ + 'get_latest_by': 'id', # gjw:指定按ID获取最新记录 + 'ordering': ['-id'], #gjw: 按ID降序排列 + 'verbose_name': 'comment', #gjw: 单数显示名称改为英文 + 'verbose_name_plural': 'comment', # gjw:复数显示名称改为英文 + }, ), + # 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'), ), - ] + ] \ No newline at end of file diff --git a/src/djangoblog-master/comments/models.py b/src/djangoblog-master/comments/models.py index 7c3bbc8..65f5c13 100644 --- a/src/djangoblog-master/comments/models.py +++ b/src/djangoblog-master/comments/models.py @@ -1,39 +1,82 @@ 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 django.utils.translation import gettext_lazy as _ #gjw: 国际化翻译 +from blog.models import Article # gjw:导入文章模型 -from blog.models import Article - - -# Create your models here. +# gjw:评论模型 class Comment(models.Model): + """ + 评论模型 + 用于存储用户对文章的评论,支持评论回复功能 + """ + + # gjw:评论正文,最大长度300字符 body = models.TextField('正文', max_length=300) + + # gjw:创建时间,默认值为当前时间 creation_time = models.DateTimeField(_('creation time'), default=now) + + # gjw:最后修改时间,默认值为当前时间 last_modify_time = models.DateTimeField(_('last modify time'), default=now) + + # gjw:评论作者,外键关联到用户模型 + # gjw:CASCADE: 用户删除时,其所有评论也会被删除 author = models.ForeignKey( - settings.AUTH_USER_MODEL, - verbose_name=_('author'), - on_delete=models.CASCADE) + settings.AUTH_USER_MODEL, # gjw:使用settings中配置的用户模型 + verbose_name=_('author'), # gjw:显示名称:作者 + on_delete=models.CASCADE # gjw:级联删除 + ) + + # gjw:关联的文章,外键关联到Article模型 + # gjw:CASCADE: 文章删除时,其所有评论也会被删除 article = models.ForeignKey( - Article, - verbose_name=_('article'), - on_delete=models.CASCADE) + Article, # gjw:关联到文章模型 + verbose_name=_('article'), # gjw:显示名称:文章 + on_delete=models.CASCADE # gjw:级联删除 + ) + + # gjw:父级评论,自关联实现评论回复功能 + # gjw:blank=True, null=True: 允许为空,表示可以是顶级评论 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) + 'self', # gjw:自关联,指向同一个模型 + verbose_name=_('parent comment'), # gjw:显示名称:父评论 + blank=True, # gjw:表单验证允许为空 + null=True, # gjw:数据库允许为NULL + on_delete=models.CASCADE #gjw: 级联删除 + ) + + # gjw:是否启用显示 + # gjw:default=False: 新评论默认不显示(需要审核) + # gjw:blank=False, null=False: 必填字段,不能为空 + is_enable = models.BooleanField( + _('enable'), # 显示名称:启用 + default=False, # 默认不显示 + blank=False, # 表单验证不允许为空 + null=False # 数据库不允许为NULL + ) class Meta: + """模型的元数据配置""" + + # gjw:默认按ID降序排列,新的评论显示在前面 ordering = ['-id'] + + # gjw:单数显示名称 verbose_name = _('comment') + + # gjw:复数显示名称(与单数相同) verbose_name_plural = verbose_name + + # gjw:指定按ID字段获取最新记录 get_latest_by = 'id' def __str__(self): - return self.body + """ + 模型的字符串表示方法 + 在Django admin和其他显示场合使用 + Returns: + str: 评论正文内容 + """ + return self.body \ No newline at end of file diff --git a/src/djangoblog-master/comments/tests.py b/src/djangoblog-master/comments/tests.py index 2a7f55f..44c5db3 100644 --- a/src/djangoblog-master/comments/tests.py +++ b/src/djangoblog-master/comments/tests.py @@ -1,68 +1,93 @@ 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 * +from comments.templatetags.comments_tags import * # gjw:导入评论模板标签 from djangoblog.utils import get_max_articleid_commentid - - -# Create your tests here. - +# gjw:评论功能测试类 class CommentsTest(TransactionTestCase): + """ + 评论系统测试类 + 测试评论的发布、回复、显示等功能 + 使用TransactionTestCase确保测试数据库事务隔离 + """ 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 value = BlogSettings() - value.comment_need_review = True + value.comment_need_review = True # gjw:开启评论审核功能 value.save() - + # gjw:创建超级用户用于测试 self.user = BlogUser.objects.create_superuser( email="liangliangyy1@gmail.com", username="liangliangyy1", password="liangliangyy1") 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: - comment.is_enable = True + comment.is_enable = True # gjw:启用评论 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' - article.status = 'p' + article.type = 'a' #gjw: 文章类型 + article.status = 'p' # gjw:发布状态 article.save() + #gjw: 获取评论提交URL comment_url = reverse( 'comments:postcomment', kwargs={ 'article_id': article.id}) + # gjw:测试提交第一条评论 response = self.client.post(comment_url, { - 'body': '123ffffffffff' + 'body': '123ffffffffff' # gjw:评论内容 }) + # gjw:验证重定向响应(评论提交后应该重定向) self.assertEqual(response.status_code, 302) + # gjw:重新获取文章对象,验证评论数量(由于需要审核,初始应为0) article = Article.objects.get(pk=article.pk) self.assertEqual(len(article.comment_list()), 0) - self.update_article_comment_status(article) + # gjw:启用评论后再次验证 + self.update_article_comment_status(article) self.assertEqual(len(article.comment_list()), 1) + # gjw:测试提交第二条评论 response = self.client.post(comment_url, { 'body': '123ffffffffff', @@ -70,11 +95,15 @@ 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': ''' @@ -89,21 +118,31 @@ class CommentsTest(TransactionTestCase): [ddd](http://www.baidu.com) - ''', - 'parent_comment_id': parent_comment_id + ''', # gjw:包含Markdown格式的评论内容 + 'parent_comment_id': parent_comment_id # gjw:父评论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) + self.assertEqual(len(tree), 1) # gjw:验证评论树结构 + + #gjw: 测试评论项显示功能 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) \ No newline at end of file diff --git a/src/djangoblog-master/comments/urls.py b/src/djangoblog-master/comments/urls.py index 7df3fab..87a381c 100644 --- a/src/djangoblog-master/comments/urls.py +++ b/src/djangoblog-master/comments/urls.py @@ -1,11 +1,21 @@ from django.urls import path -from . import views +from . import views # gjw:导入当前应用的视图模块 +# gjw:定义应用命名空间,用于URL反向解析时区分不同应用的相同URL名称 app_name = "comments" + +# gjw:URL模式配置列表 urlpatterns = [ + # 评论提交URL配置 path( + # gjw:URL模式:/article/<文章ID>/postcomment 'article/Thank you very much for your comments on this site
You can visit %(article_title)s to review your comments, Thank you again!