diff --git a/src/DjangoBlog/accounts/admin.py b/src/DjangoBlog/accounts/admin.py index 29d162a..60e4118 100644 --- a/src/DjangoBlog/accounts/admin.py +++ b/src/DjangoBlog/accounts/admin.py @@ -9,15 +9,17 @@ from .models import BlogUser class BlogUserCreationForm(forms.ModelForm): + # 密码输入字段1 password1 = forms.CharField(label=_('password'), widget=forms.PasswordInput) + # 密码确认字段2 password2 = forms.CharField(label=_('Enter password again'), widget=forms.PasswordInput) class Meta: - model = BlogUser - fields = ('email',) + model = BlogUser # 指定模型为BlogUser + fields = ('email',) # 只包含email字段 def clean_password2(self): - # Check that the two password entries match + # 验证两次输入的密码是否一致 password1 = self.cleaned_data.get("password1") password2 = self.cleaned_data.get("password2") if password1 and password2 and password1 != password2: @@ -25,28 +27,30 @@ class BlogUserCreationForm(forms.ModelForm): return password2 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: - user.source = 'adminsite' + user.source = 'adminsite' # 设置用户来源为管理员站点 user.save() return user class BlogUserChangeForm(UserChangeForm): class Meta: - model = BlogUser - fields = '__all__' - field_classes = {'username': UsernameField} + model = BlogUser # 指定模型为BlogUser + fields = '__all__' # 包含所有字段 + field_classes = {'username': UsernameField} # 指定username字段类型 def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) + super().__init__(*args, **kwargs) # 调用父类初始化方法 class BlogUserAdmin(UserAdmin): - form = BlogUserChangeForm - add_form = BlogUserCreationForm + # 使用自定义的表单类 + form = BlogUserChangeForm # 修改表单 + add_form = BlogUserCreationForm # 添加表单 + # 列表页显示的字段 list_display = ( 'id', 'nickname', @@ -55,6 +59,9 @@ class BlogUserAdmin(UserAdmin): 'last_login', 'date_joined', 'source') + # 列表页可点击的链接字段 list_display_links = ('id', 'username') + # 默认排序字段 ordering = ('-id',) - search_fields = ('username', 'nickname', 'email') + # 搜索字段 + search_fields = ('username', 'nickname', 'email') \ No newline at end of file diff --git a/src/DjangoBlog/accounts/apps.py b/src/DjangoBlog/accounts/apps.py index 9b3fc5a..d6e8a46 100644 --- a/src/DjangoBlog/accounts/apps.py +++ b/src/DjangoBlog/accounts/apps.py @@ -2,4 +2,4 @@ from django.apps import AppConfig class AccountsConfig(AppConfig): - name = 'accounts' + name = 'accounts' # 指定应用的Python路径为'accounts' \ No newline at end of file diff --git a/src/DjangoBlog/accounts/forms.py b/src/DjangoBlog/accounts/forms.py index fce4137..70289b9 100644 --- a/src/DjangoBlog/accounts/forms.py +++ b/src/DjangoBlog/accounts/forms.py @@ -11,8 +11,10 @@ from .models import BlogUser class LoginForm(AuthenticationForm): def __init__(self, *args, **kwargs): super(LoginForm, self).__init__(*args, **kwargs) + # 设置用户名输入框的HTML属性 self.fields['username'].widget = widgets.TextInput( attrs={'placeholder': "username", "class": "form-control"}) + # 设置密码输入框的HTML属性 self.fields['password'].widget = widgets.PasswordInput( attrs={'placeholder': "password", "class": "form-control"}) @@ -21,6 +23,7 @@ class RegisterForm(UserCreationForm): def __init__(self, *args, **kwargs): super(RegisterForm, self).__init__(*args, **kwargs) + # 设置各字段输入框的HTML属性 self.fields['username'].widget = widgets.TextInput( attrs={'placeholder': "username", "class": "form-control"}) self.fields['email'].widget = widgets.EmailInput( @@ -31,17 +34,19 @@ class RegisterForm(UserCreationForm): attrs={'placeholder': "repeat password", "class": "form-control"}) def clean_email(self): + # 验证邮箱是否已存在 email = self.cleaned_data['email'] if get_user_model().objects.filter(email=email).exists(): raise ValidationError(_("email already exists")) return email class Meta: - model = get_user_model() - fields = ("username", "email") + model = get_user_model() # 获取当前用户模型 + fields = ("username", "email") # 表单包含的字段 class ForgetPasswordForm(forms.Form): + # 新密码字段 new_password1 = forms.CharField( label=_("New password"), widget=forms.PasswordInput( @@ -52,6 +57,7 @@ class ForgetPasswordForm(forms.Form): ), ) + # 确认密码字段 new_password2 = forms.CharField( label="确认密码", widget=forms.PasswordInput( @@ -62,6 +68,7 @@ class ForgetPasswordForm(forms.Form): ), ) + # 邮箱字段 email = forms.EmailField( label='邮箱', widget=forms.TextInput( @@ -72,6 +79,7 @@ class ForgetPasswordForm(forms.Form): ), ) + # 验证码字段 code = forms.CharField( label=_('Code'), widget=forms.TextInput( @@ -83,15 +91,18 @@ class ForgetPasswordForm(forms.Form): ) def clean_new_password2(self): + # 验证两次输入的新密码是否一致 password1 = self.data.get("new_password1") password2 = self.data.get("new_password2") if password1 and password2 and password1 != password2: raise ValidationError(_("passwords do not match")) + # 验证密码强度 password_validation.validate_password(password2) return password2 def clean_email(self): + # 验证邮箱是否存在 user_email = self.cleaned_data.get("email") if not BlogUser.objects.filter( email=user_email @@ -101,6 +112,7 @@ class ForgetPasswordForm(forms.Form): return user_email def clean_code(self): + # 验证验证码是否正确 code = self.cleaned_data.get("code") error = utils.verify( email=self.cleaned_data.get("email"), @@ -112,6 +124,7 @@ class ForgetPasswordForm(forms.Form): class ForgetPasswordCodeForm(forms.Form): + # 忘记密码时的邮箱验证表单 email = forms.EmailField( label=_('Email'), - ) + ) \ No newline at end of file diff --git a/src/DjangoBlog/accounts/migrations/0001_initial.py b/src/DjangoBlog/accounts/migrations/0001_initial.py index d2fbcab..5d48851 100644 --- a/src/DjangoBlog/accounts/migrations/0001_initial.py +++ b/src/DjangoBlog/accounts/migrations/0001_initial.py @@ -8,42 +8,60 @@ import django.utils.timezone class Migration(migrations.Migration): - initial = True + initial = True # 标记这是初始迁移 dependencies = [ - ('auth', '0012_alter_user_first_name_max_length'), + ('auth', '0012_alter_user_first_name_max_length'), # 依赖auth应用的迁移 ] operations = [ migrations.CreateModel( - name='BlogUser', + name='BlogUser', # 创建自定义用户模型BlogUser fields=[ + # 主键ID,自增BigAutoField ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + # 密码字段,使用Django的密码哈希存储 ('password', models.CharField(max_length=128, verbose_name='password')), + # 最后登录时间,可为空 ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), + # 超级用户标志位 ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), + # 用户名字段,具有唯一性验证和字符验证 ('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')), + # 名字字段,可为空 ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')), + # 姓氏字段,可为空 ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), + # 邮箱字段,可为空 ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), + # 员工状态标志位,决定是否能登录admin后台 ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), + # 活跃状态标志位,用于软删除 ('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')), + # 账户创建时间,默认为当前时间 ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), + # 自定义字段:昵称 ('nickname', models.CharField(blank=True, max_length=100, verbose_name='昵称')), + # 自定义字段:创建时间 ('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')), + # 自定义字段:最后修改时间 ('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')), + # 自定义字段:创建来源 ('source', models.CharField(blank=True, max_length=100, verbose_name='创建来源')), + # 用户组多对多关系 ('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')), + # 用户权限多对多关系 ('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={ - 'verbose_name': '用户', - 'verbose_name_plural': '用户', - 'ordering': ['-id'], - 'get_latest_by': 'id', + 'verbose_name': '用户', # 单数显示名称 + 'verbose_name_plural': '用户', # 复数显示名称 + 'ordering': ['-id'], # 默认按ID倒序排列 + 'get_latest_by': 'id', # 指定获取最新记录的字段 }, managers=[ + # 使用Django原生的UserManager管理用户对象 ('objects', django.contrib.auth.models.UserManager()), ], ), - ] + ] \ No newline at end of file diff --git a/src/DjangoBlog/accounts/migrations/0002_alter_bloguser_options_remove_bloguser_created_time_and_more.py b/src/DjangoBlog/accounts/migrations/0002_alter_bloguser_options_remove_bloguser_created_time_and_more.py index 1a9f509..3a414b1 100644 --- a/src/DjangoBlog/accounts/migrations/0002_alter_bloguser_options_remove_bloguser_created_time_and_more.py +++ b/src/DjangoBlog/accounts/migrations/0002_alter_bloguser_options_remove_bloguser_created_time_and_more.py @@ -7,40 +7,47 @@ import django.utils.timezone class Migration(migrations.Migration): dependencies = [ - ('accounts', '0001_initial'), + ('accounts', '0001_initial'), # 依赖accounts应用的初始迁移 ] operations = [ + # 修改BlogUser模型的元数据选项 migrations.AlterModelOptions( name='bloguser', options={'get_latest_by': 'id', 'ordering': ['-id'], 'verbose_name': 'user', 'verbose_name_plural': 'user'}, ), + # 删除created_time字段 migrations.RemoveField( model_name='bloguser', name='created_time', ), + # 删除last_mod_time字段 migrations.RemoveField( model_name='bloguser', name='last_mod_time', ), + # 新增creation_time字段 migrations.AddField( model_name='bloguser', name='creation_time', field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'), ), + # 新增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'), ), + # 修改nickname字段的verbose_name migrations.AlterField( model_name='bloguser', name='nickname', field=models.CharField(blank=True, max_length=100, verbose_name='nick name'), ), + # 修改source字段的verbose_name migrations.AlterField( model_name='bloguser', name='source', field=models.CharField(blank=True, max_length=100, verbose_name='create source'), ), - ] + ] \ No newline at end of file diff --git a/src/DjangoBlog/accounts/models.py b/src/DjangoBlog/accounts/models.py index 3baddbb..a1f0e18 100644 --- a/src/DjangoBlog/accounts/models.py +++ b/src/DjangoBlog/accounts/models.py @@ -9,27 +9,34 @@ from djangoblog.utils import get_current_site # Create your models here. class BlogUser(AbstractUser): + # 昵称字段,最大长度100,可为空 nickname = models.CharField(_('nick name'), max_length=100, blank=True) + # 创建时间字段,默认为当前时间 creation_time = models.DateTimeField(_('creation time'), default=now) + # 最后修改时间字段,默认为当前时间 last_modify_time = models.DateTimeField(_('last modify time'), default=now) + # 创建来源字段,最大长度100,可为空 source = models.CharField(_('create source'), max_length=100, blank=True) def get_absolute_url(self): + # 获取用户详情页的URL return reverse( 'blog:author_detail', kwargs={ 'author_name': self.username}) def __str__(self): + # 对象的字符串表示,返回邮箱 return self.email def get_full_url(self): + # 获取完整的用户URL(包含域名) site = get_current_site().domain url = "https://{site}{path}".format(site=site, path=self.get_absolute_url()) return url class Meta: - ordering = ['-id'] - verbose_name = _('user') - verbose_name_plural = verbose_name - get_latest_by = 'id' + ordering = ['-id'] # 按ID倒序排列 + verbose_name = _('user') # 单数显示名称 + verbose_name_plural = verbose_name # 复数显示名称(与单数相同) + get_latest_by = 'id' # 指定获取最新记录的字段 \ No newline at end of file diff --git a/src/DjangoBlog/accounts/tests.py b/src/DjangoBlog/accounts/tests.py index 6893411..3e872a6 100644 --- a/src/DjangoBlog/accounts/tests.py +++ b/src/DjangoBlog/accounts/tests.py @@ -13,35 +13,37 @@ from . import utils class AccountTest(TestCase): def setUp(self): - self.client = Client() - self.factory = RequestFactory() + # 测试初始化设置 + self.client = Client() # 创建测试客户端 + self.factory = RequestFactory() # 创建请求工厂 self.blog_user = BlogUser.objects.create_user( username="test", email="admin@admin.com", password="12345678" - ) - self.new_test = "xxx123--=" + ) # 创建测试用户 + self.new_test = "xxx123--=" # 新密码测试字符串 def test_validate_account(self): + # 测试账户验证功能 site = get_current_site().domain user = BlogUser.objects.create_superuser( email="liangliangyy1@gmail.com", username="liangliangyy1", - password="qwer!@#$ggg") + password="qwer!@#$ggg") # 创建超级用户 testuser = BlogUser.objects.get(username='liangliangyy1') loginresult = self.client.login( username='liangliangyy1', - password='qwer!@#$ggg') - self.assertEqual(loginresult, True) - response = self.client.get('/admin/') - self.assertEqual(response.status_code, 200) + password='qwer!@#$ggg') # 测试登录 + self.assertEqual(loginresult, True) # 验证登录成功 + response = self.client.get('/admin/') # 访问管理后台 + self.assertEqual(response.status_code, 200) # 验证访问成功 category = Category() category.name = "categoryaaa" category.creation_time = timezone.now() category.last_modify_time = timezone.now() - category.save() + category.save() # 创建测试分类 article = Article() article.title = "nicetitleaaa" @@ -50,45 +52,46 @@ class AccountTest(TestCase): article.category = category article.type = 'a' article.status = 'p' - article.save() + article.save() # 创建测试文章 - response = self.client.get(article.get_admin_url()) - self.assertEqual(response.status_code, 200) + response = self.client.get(article.get_admin_url()) # 访问文章管理页面 + self.assertEqual(response.status_code, 200) # 验证访问成功 def test_validate_register(self): + # 测试用户注册功能 self.assertEquals( 0, len( BlogUser.objects.filter( - email='user123@user.com'))) + email='user123@user.com'))) # 验证用户不存在 response = self.client.post(reverse('account:register'), { 'username': 'user1233', 'email': 'user123@user.com', 'password1': 'password123!q@wE#R$T', 'password2': 'password123!q@wE#R$T', - }) + }) # 提交注册表单 self.assertEquals( 1, len( BlogUser.objects.filter( - email='user123@user.com'))) + email='user123@user.com'))) # 验证用户创建成功 user = BlogUser.objects.filter(email='user123@user.com')[0] - 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') url = '{path}?type=validation&id={id}&sign={sign}'.format( - path=path, id=user.id, sign=sign) - response = self.client.get(url) - self.assertEqual(response.status_code, 200) + path=path, id=user.id, sign=sign) # 构建验证URL + response = self.client.get(url) # 访问验证页面 + self.assertEqual(response.status_code, 200) # 验证访问成功 - 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.is_superuser = True - user.is_staff = True + user.is_superuser = True # 设置为超级用户 + user.is_staff = True # 设置为员工 user.save() - delete_sidebar_cache() + delete_sidebar_cache() # 清除侧边栏缓存 category = Category() category.name = "categoryaaa" category.creation_time = timezone.now() category.last_modify_time = timezone.now() - category.save() + category.save() # 创建分类 article = Article() article.category = category @@ -98,110 +101,114 @@ class AccountTest(TestCase): article.type = 'a' article.status = 'p' - article.save() + article.save() # 创建文章 - response = self.client.get(article.get_admin_url()) - self.assertEqual(response.status_code, 200) + response = self.client.get(article.get_admin_url()) # 访问文章管理页面 + self.assertEqual(response.status_code, 200) # 验证访问成功 - response = self.client.get(reverse('account:logout')) - self.assertIn(response.status_code, [301, 302, 200]) + response = self.client.get(reverse('account:logout')) # 退出登录 + self.assertIn(response.status_code, [301, 302, 200]) # 验证重定向 - response = self.client.get(article.get_admin_url()) - self.assertIn(response.status_code, [301, 302, 200]) + response = self.client.get(article.get_admin_url()) # 再次访问管理页面 + self.assertIn(response.status_code, [301, 302, 200]) # 验证被重定向(未登录) response = self.client.post(reverse('account:login'), { 'username': 'user1233', 'password': 'password123' - }) - self.assertIn(response.status_code, [301, 302, 200]) + }) # 重新登录 + self.assertIn(response.status_code, [301, 302, 200]) # 验证登录重定向 - response = self.client.get(article.get_admin_url()) - self.assertIn(response.status_code, [301, 302, 200]) + response = self.client.get(article.get_admin_url()) # 再次访问管理页面 + self.assertIn(response.status_code, [301, 302, 200]) # 验证访问成功 def test_verify_email_code(self): + # 测试邮箱验证码功能 to_email = "admin@admin.com" - code = generate_code() - utils.set_code(to_email, code) - utils.send_verify_email(to_email, code) + code = generate_code() # 生成验证码 + utils.set_code(to_email, code) # 设置验证码 + utils.send_verify_email(to_email, code) # 发送验证邮件 - err = utils.verify("admin@admin.com", code) - self.assertEqual(err, None) + err = utils.verify("admin@admin.com", code) # 验证正确邮箱和验证码 + self.assertEqual(err, None) # 验证无错误 - err = utils.verify("admin@123.com", code) - self.assertEqual(type(err), str) + err = utils.verify("admin@123.com", code) # 验证错误邮箱 + self.assertEqual(type(err), str) # 验证返回错误信息 def test_forget_password_email_code_success(self): + # 测试忘记密码邮箱验证码成功情况 resp = self.client.post( path=reverse("account:forget_password_code"), data=dict(email="admin@admin.com") - ) + ) # 提交忘记密码请求 - self.assertEqual(resp.status_code, 200) - self.assertEqual(resp.content.decode("utf-8"), "ok") + self.assertEqual(resp.status_code, 200) # 验证响应成功 + self.assertEqual(resp.content.decode("utf-8"), "ok") # 验证返回内容 def test_forget_password_email_code_fail(self): + # 测试忘记密码邮箱验证码失败情况 resp = self.client.post( path=reverse("account:forget_password_code"), data=dict() - ) - self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱") + ) # 提交空数据 + self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱") # 验证错误信息 resp = self.client.post( path=reverse("account:forget_password_code"), data=dict(email="admin@com") - ) - self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱") + ) # 提交无效邮箱 + self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱") # 验证错误信息 def test_forget_password_email_success(self): - code = generate_code() - utils.set_code(self.blog_user.email, code) + # 测试忘记密码成功情况 + code = generate_code() # 生成验证码 + utils.set_code(self.blog_user.email, code) # 设置验证码 data = dict( new_password1=self.new_test, new_password2=self.new_test, email=self.blog_user.email, code=code, - ) + ) # 准备重置密码数据 resp = self.client.post( path=reverse("account:forget_password"), data=data - ) - self.assertEqual(resp.status_code, 302) + ) # 提交重置密码请求 + self.assertEqual(resp.status_code, 302) # 验证重定向响应 # 验证用户密码是否修改成功 blog_user = BlogUser.objects.filter( email=self.blog_user.email, - ).first() # type: BlogUser - self.assertNotEqual(blog_user, None) - self.assertEqual(blog_user.check_password(data["new_password1"]), True) + ).first() # type: BlogUser # 获取用户 + self.assertNotEqual(blog_user, None) # 验证用户存在 + self.assertEqual(blog_user.check_password(data["new_password1"]), True) # 验证密码修改成功 def test_forget_password_email_not_user(self): + # 测试忘记密码时用户不存在的情况 data = dict( new_password1=self.new_test, new_password2=self.new_test, email="123@123.com", code="123456", - ) + ) # 准备不存在的用户数据 resp = self.client.post( path=reverse("account:forget_password"), data=data - ) - - self.assertEqual(resp.status_code, 200) + ) # 提交重置密码请求 + self.assertEqual(resp.status_code, 200) # 验证响应成功(但应该显示错误) def test_forget_password_email_code_error(self): - code = generate_code() - utils.set_code(self.blog_user.email, code) + # 测试忘记密码验证码错误的情况 + code = generate_code() # 生成验证码 + utils.set_code(self.blog_user.email, code) # 设置正确验证码 data = dict( new_password1=self.new_test, new_password2=self.new_test, email=self.blog_user.email, - code="111111", - ) + code="111111", # 使用错误验证码 + ) # 准备重置密码数据 resp = self.client.post( path=reverse("account:forget_password"), data=data - ) - - self.assertEqual(resp.status_code, 200) + ) # 提交重置密码请求 + self.assertEqual(resp.status_code, 200) # 验证响应成功(但应该显示验证码错误) \ No newline at end of file diff --git a/src/DjangoBlog/accounts/urls.py b/src/DjangoBlog/accounts/urls.py index 107a801..2956136 100644 --- a/src/DjangoBlog/accounts/urls.py +++ b/src/DjangoBlog/accounts/urls.py @@ -4,25 +4,32 @@ from django.urls import re_path from . import views from .forms import LoginForm -app_name = "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'), - ] +urlpatterns = [ + # 登录URL,使用LoginForm作为认证表单,成功跳转到首页 + re_path(r'^login/$', + views.LoginView.as_view(success_url='/'), + name='login', + kwargs={'authentication_form': LoginForm}), + # 注册URL,注册成功跳转到首页 + re_path(r'^register/$', + views.RegisterView.as_view(success_url="/"), + name='register'), + # 退出登录URL + re_path(r'^logout/$', + views.LogoutView.as_view(), + name='logout'), + # 账户操作结果页面URL + path(r'account/result.html', + views.account_result, + name='result'), + # 忘记密码页面URL + re_path(r'^forget_password/$', + views.ForgetPasswordView.as_view(), + name='forget_password'), + # 获取忘记密码验证码URL + re_path(r'^forget_password_code/$', + views.ForgetPasswordEmailCode.as_view(), + name='forget_password_code'), +] \ No newline at end of file diff --git a/src/DjangoBlog/accounts/user_login_backend.py b/src/DjangoBlog/accounts/user_login_backend.py index 73cdca1..270d72e 100644 --- a/src/DjangoBlog/accounts/user_login_backend.py +++ b/src/DjangoBlog/accounts/user_login_backend.py @@ -8,19 +8,25 @@ class EmailOrUsernameModelBackend(ModelBackend): """ def authenticate(self, request, username=None, password=None, **kwargs): + # 判断输入是否包含@符号,决定使用邮箱还是用户名进行验证 if '@' in username: - kwargs = {'email': username} + kwargs = {'email': username} # 如果包含@,按邮箱处理 else: - kwargs = {'username': username} + kwargs = {'username': username} # 否则按用户名处理 try: + # 根据用户名或邮箱查找用户 user = get_user_model().objects.get(**kwargs) + # 验证密码是否正确 if user.check_password(password): return user except get_user_model().DoesNotExist: + # 用户不存在时返回None return None def get_user(self, username): try: + # 根据用户ID获取用户对象 return get_user_model().objects.get(pk=username) except get_user_model().DoesNotExist: - return None + # 用户不存在时返回None + return None \ No newline at end of file diff --git a/src/DjangoBlog/accounts/utils.py b/src/DjangoBlog/accounts/utils.py index 4b94bdf..5d31e35 100644 --- a/src/DjangoBlog/accounts/utils.py +++ b/src/DjangoBlog/accounts/utils.py @@ -7,7 +7,7 @@ from django.utils.translation import gettext_lazy as _ from djangoblog.utils import send_email -_code_ttl = timedelta(minutes=5) +_code_ttl = timedelta(minutes=5) # 验证码有效期为5分钟 def send_verify_email(to_mail: str, code: str, subject: str = _("Verify Email")): @@ -19,8 +19,8 @@ def send_verify_email(to_mail: str, code: str, subject: str = _("Verify Email")) """ html_content = _( "You are resetting the password, the verification code is:%(code)s, valid within 5 minutes, please keep it " - "properly") % {'code': code} - send_email([to_mail], subject, html_content) + "properly") % {'code': code} # 邮件内容模板,包含验证码和有效期提示 + send_email([to_mail], subject, html_content) # 调用发送邮件函数 def verify(email: str, code: str) -> typing.Optional[str]: @@ -34,16 +34,16 @@ def verify(email: str, code: str) -> typing.Optional[str]: 这里的错误处理不太合理,应该采用raise抛出 否测调用方也需要对error进行处理 """ - cache_code = get_code(email) - if cache_code != code: - return gettext("Verification code error") + cache_code = get_code(email) # 从缓存获取该邮箱对应的验证码 + if cache_code != code: # 比较缓存中的验证码和用户输入的验证码 + return gettext("Verification code error") # 验证码不匹配时返回错误信息 def set_code(email: str, code: str): """设置code""" - cache.set(email, code, _code_ttl.seconds) + cache.set(email, code, _code_ttl.seconds) # 将验证码存入缓存,设置过期时间为5分钟 def get_code(email: str) -> typing.Optional[str]: """获取code""" - return cache.get(email) + return cache.get(email) # 从缓存获取指定邮箱的验证码 \ No newline at end of file diff --git a/src/DjangoBlog/accounts/views.py b/src/DjangoBlog/accounts/views.py index ae67aec..8973e4b 100644 --- a/src/DjangoBlog/accounts/views.py +++ b/src/DjangoBlog/accounts/views.py @@ -32,27 +32,27 @@ logger = logging.getLogger(__name__) # Create your views here. class RegisterView(FormView): - form_class = RegisterForm - template_name = 'account/registration_form.html' + form_class = RegisterForm # 使用注册表单 + template_name = 'account/registration_form.html' # 注册模板 - @method_decorator(csrf_protect) + @method_decorator(csrf_protect) # CSRF保护装饰器 def dispatch(self, *args, **kwargs): return super(RegisterView, self).dispatch(*args, **kwargs) def form_valid(self, form): if form.is_valid(): - user = form.save(False) - user.is_active = False - user.source = 'Register' - user.save(True) - site = get_current_site().domain - sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id))) + user = form.save(False) # 不立即保存用户 + user.is_active = False # 设置用户为非活跃状态 + user.source = 'Register' # 设置用户来源为注册 + user.save(True) # 保存用户到数据库 + site = get_current_site().domain # 获取当前站点域名 + sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id))) # 生成验证签名 if settings.DEBUG: - site = '127.0.0.1:8000' + site = '127.0.0.1:8000' # 调试模式下使用本地地址 path = reverse('account:result') 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) # 构建验证URL content = """

请点击下面链接验证您的邮箱

@@ -63,142 +63,139 @@ class RegisterView(FormView):
如果上面链接无法打开,请将此链接复制至浏览器。 {url} - """.format(url=url) + """.format(url=url) # 邮件内容模板 send_email( emailto=[ user.email, ], title='验证您的电子邮箱', - content=content) + content=content) # 发送验证邮件 url = reverse('accounts:result') + \ - '?type=register&id=' + str(user.id) - return HttpResponseRedirect(url) + '?type=register&id=' + str(user.id) # 构建注册结果URL + return HttpResponseRedirect(url) # 重定向到结果页面 else: return self.render_to_response({ - 'form': form + 'form': form # 表单验证失败,重新渲染表单 }) class LogoutView(RedirectView): - url = '/login/' + url = '/login/' # 退出后重定向到登录页 - @method_decorator(never_cache) + @method_decorator(never_cache) # 禁止缓存装饰器 def dispatch(self, request, *args, **kwargs): return super(LogoutView, self).dispatch(request, *args, **kwargs) def get(self, request, *args, **kwargs): - logout(request) - delete_sidebar_cache() - return super(LogoutView, self).get(request, *args, **kwargs) + logout(request) # 执行退出登录 + delete_sidebar_cache() # 删除侧边栏缓存 + return super(LogoutView, self).get(request, *args, **kwargs) # 重定向到登录页 class LoginView(FormView): - form_class = LoginForm - template_name = 'account/login.html' - success_url = '/' - redirect_field_name = REDIRECT_FIELD_NAME - login_ttl = 2626560 # 一个月的时间 - - @method_decorator(sensitive_post_parameters('password')) - @method_decorator(csrf_protect) - @method_decorator(never_cache) + form_class = LoginForm # 使用登录表单 + template_name = 'account/login.html' # 登录模板 + success_url = '/' # 登录成功默认跳转首页 + redirect_field_name = REDIRECT_FIELD_NAME # 重定向字段名 + login_ttl = 2626560 # 一个月的时间(会话有效期) + + @method_decorator(sensitive_post_parameters('password')) # 敏感参数保护 + @method_decorator(csrf_protect) # CSRF保护 + @method_decorator(never_cache) # 禁止缓存 def dispatch(self, request, *args, **kwargs): - return super(LoginView, self).dispatch(request, *args, **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) # 获取重定向URL if redirect_to is None: - 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) # 返回上下文数据 def form_valid(self, form): - form = AuthenticationForm(data=self.request.POST, request=self.request) + form = AuthenticationForm(data=self.request.POST, request=self.request) # 使用Django认证表单 if form.is_valid(): - delete_sidebar_cache() + delete_sidebar_cache() # 删除侧边栏缓存 logger.info(self.redirect_field_name) - auth.login(self.request, form.get_user()) - if self.request.POST.get("remember"): - self.request.session.set_expiry(self.login_ttl) - return super(LoginView, self).form_valid(form) - # return HttpResponseRedirect('/') + auth.login(self.request, form.get_user()) # 执行登录操作 + if self.request.POST.get("remember"): # 检查"记住我"选项 + self.request.session.set_expiry(self.login_ttl) # 设置会话有效期 + return super(LoginView, self).form_valid(form) # 调用父类form_valid方法 else: return self.render_to_response({ - 'form': form + 'form': form # 表单验证失败,重新渲染表单 }) 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) # 从POST获取重定向URL if not url_has_allowed_host_and_scheme( url=redirect_to, allowed_hosts=[ - self.request.get_host()]): - redirect_to = self.success_url - return redirect_to + self.request.get_host()]): # 验证URL安全性 + redirect_to = self.success_url # 使用默认成功URL + return redirect_to # 返回重定向URL def account_result(request): - type = request.GET.get('type') - id = request.GET.get('id') + type = request.GET.get('type') # 获取操作类型 + id = request.GET.get('id') # 获取用户ID - user = get_object_or_404(get_user_model(), id=id) + user = get_object_or_404(get_user_model(), id=id) # 获取用户对象,不存在返回404 logger.info(type) - if user.is_active: + if user.is_active: # 如果用户已激活,重定向到首页 return HttpResponseRedirect('/') - if type and type in ['register', 'validation']: + if type and type in ['register', 'validation']: # 检查操作类型 if type == 'register': content = ''' 恭喜您注册成功,一封验证邮件已经发送到您的邮箱,请验证您的邮箱后登录本站。 - ''' + ''' # 注册成功提示 title = '注册成功' else: - c_sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id))) - sign = request.GET.get('sign') - if sign != c_sign: - return HttpResponseForbidden() - user.is_active = True - user.save() + c_sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id))) # 计算验证签名 + sign = request.GET.get('sign') # 获取请求中的签名 + if sign != c_sign: # 验证签名是否匹配 + return HttpResponseForbidden() # 签名不匹配返回403 + user.is_active = True # 激活用户 + user.save() # 保存用户状态 content = ''' 恭喜您已经成功的完成邮箱验证,您现在可以使用您的账号来登录本站。 - ''' + ''' # 验证成功提示 title = '验证成功' return render(request, 'account/result.html', { 'title': title, - 'content': content + 'content': content # 渲染结果页面 }) else: - return HttpResponseRedirect('/') + return HttpResponseRedirect('/') # 无效类型重定向到首页 class ForgetPasswordView(FormView): - form_class = ForgetPasswordForm - template_name = 'account/forget_password.html' + form_class = ForgetPasswordForm # 使用忘记密码表单 + template_name = 'account/forget_password.html' # 忘记密码模板 def form_valid(self, form): if form.is_valid(): - blog_user = BlogUser.objects.filter(email=form.cleaned_data.get("email")).get() - blog_user.password = make_password(form.cleaned_data["new_password2"]) - blog_user.save() - return HttpResponseRedirect('/login/') + blog_user = BlogUser.objects.filter(email=form.cleaned_data.get("email")).get() # 根据邮箱获取用户 + blog_user.password = make_password(form.cleaned_data["new_password2"]) # 加密新密码 + blog_user.save() # 保存用户新密码 + return HttpResponseRedirect('/login/') # 重定向到登录页 else: - return self.render_to_response({'form': form}) + return self.render_to_response({'form': form}) # 表单验证失败,重新渲染表单 class ForgetPasswordEmailCode(View): def post(self, request: HttpRequest): - form = ForgetPasswordCodeForm(request.POST) - if not form.is_valid(): - return HttpResponse("错误的邮箱") - to_email = form.cleaned_data["email"] + form = ForgetPasswordCodeForm(request.POST) # 创建忘记密码验证码表单 + if not form.is_valid(): # 表单验证 + return HttpResponse("错误的邮箱") # 邮箱格式错误 + to_email = form.cleaned_data["email"] # 获取邮箱地址 - code = generate_code() - utils.send_verify_email(to_email, code) - utils.set_code(to_email, code) + code = generate_code() # 生成验证码 + utils.send_verify_email(to_email, code) # 发送验证邮件 + utils.set_code(to_email, code) # 保存验证码到缓存 - return HttpResponse("ok") + return HttpResponse("ok") # 返回成功响应 \ No newline at end of file diff --git a/src/DjangoBlog/blog/admin.py b/src/DjangoBlog/blog/admin.py index 69d7f8e..7c2ab60 100644 --- a/src/DjangoBlog/blog/admin.py +++ b/src/DjangoBlog/blog/admin.py @@ -10,105 +10,113 @@ from .models import Article, Category, Tag, Links, SideBar, BlogSettings class ArticleForm(forms.ModelForm): - # body = forms.CharField(widget=AdminPagedownWidget()) + # body = forms.CharField(widget=AdminPagedownWidget()) # 注释掉的Markdown编辑器部件 class Meta: - model = Article - fields = '__all__' + model = Article # 指定模型为Article + fields = '__all__' # 包含所有字段 def makr_article_publish(modeladmin, request, queryset): - queryset.update(status='p') + # 发布选中文章的动作 + queryset.update(status='p') # 将状态设置为已发布 def draft_article(modeladmin, request, queryset): - queryset.update(status='d') + # 将选中文章设为草稿的动作 + queryset.update(status='d') # 将状态设置为草稿 def close_article_commentstatus(modeladmin, request, queryset): - queryset.update(comment_status='c') + # 关闭选中文章评论的动作 + queryset.update(comment_status='c') # 将评论状态设置为关闭 def open_article_commentstatus(modeladmin, request, queryset): - queryset.update(comment_status='o') + # 打开选中文章评论的动作 + queryset.update(comment_status='o') # 将评论状态设置为打开 -makr_article_publish.short_description = _('Publish selected articles') -draft_article.short_description = _('Draft selected articles') -close_article_commentstatus.short_description = _('Close article comments') -open_article_commentstatus.short_description = _('Open article comments') +makr_article_publish.short_description = _('Publish selected articles') # 动作显示名称 +draft_article.short_description = _('Draft selected articles') # 动作显示名称 +close_article_commentstatus.short_description = _('Close article comments') # 动作显示名称 +open_article_commentstatus.short_description = _('Open article comments') # 动作显示名称 class ArticlelAdmin(admin.ModelAdmin): - list_per_page = 20 - search_fields = ('body', 'title') - form = ArticleForm + list_per_page = 20 # 每页显示20条记录 + search_fields = ('body', 'title') # 搜索字段 + form = ArticleForm # 使用自定义表单 list_display = ( 'id', 'title', 'author', - 'link_to_category', + 'link_to_category', # 自定义链接字段 'creation_time', 'views', 'status', 'type', - 'article_order') - list_display_links = ('id', 'title') - list_filter = ('status', 'type', 'category') - date_hierarchy = 'creation_time' - filter_horizontal = ('tags',) - exclude = ('creation_time', 'last_modify_time') - view_on_site = True + 'article_order') # 列表页显示的字段 + list_display_links = ('id', 'title') # 可点击链接的字段 + list_filter = ('status', 'type', 'category') # 右侧过滤器 + date_hierarchy = 'creation_time' # 日期层级导航 + filter_horizontal = ('tags',) # 水平多对多选择器 + exclude = ('creation_time', 'last_modify_time') # 排除的字段 + view_on_site = True # 启用"在站点查看"功能 actions = [ makr_article_publish, draft_article, close_article_commentstatus, - open_article_commentstatus] - raw_id_fields = ('author', 'category',) + open_article_commentstatus] # 管理员动作列表 + raw_id_fields = ('author', 'category',) # 使用原始ID字段(外键搜索) def link_to_category(self, obj): - info = (obj.category._meta.app_label, obj.category._meta.model_name) - link = reverse('admin:%s_%s_change' % info, args=(obj.category.id,)) - return format_html(u'%s' % (link, obj.category.name)) + # 创建分类链接的自定义方法 + info = (obj.category._meta.app_label, obj.category._meta.model_name) # 获取分类模型信息 + link = reverse('admin:%s_%s_change' % info, args=(obj.category.id,)) # 生成分类编辑链接 + return format_html(u'%s' % (link, obj.category.name)) # 返回HTML链接 - link_to_category.short_description = _('category') + link_to_category.short_description = _('category') # 字段显示名称 def get_form(self, request, obj=None, **kwargs): - form = super(ArticlelAdmin, self).get_form(request, obj, **kwargs) + # 自定义表单获取方法 + form = super(ArticlelAdmin, self).get_form(request, obj, **kwargs) # 调用父类方法 form.base_fields['author'].queryset = get_user_model( - ).objects.filter(is_superuser=True) + ).objects.filter(is_superuser=True) # 限制作者只能选择超级用户 return form def save_model(self, request, obj, form, change): - super(ArticlelAdmin, self).save_model(request, obj, form, change) + # 保存模型方法 + super(ArticlelAdmin, self).save_model(request, obj, form, change) # 调用父类保存方法 def get_view_on_site_url(self, obj=None): + # 获取"在站点查看"链接的方法 if obj: - url = obj.get_full_url() + url = obj.get_full_url() # 获取文章完整URL return url else: from djangoblog.utils import get_current_site - site = get_current_site().domain + site = get_current_site().domain # 获取当前站点域名 return site class TagAdmin(admin.ModelAdmin): - exclude = ('slug', 'last_mod_time', 'creation_time') + exclude = ('slug', 'last_modify_time', 'creation_time') # 排除自动生成的字段 class CategoryAdmin(admin.ModelAdmin): - list_display = ('name', 'parent_category', 'index') - exclude = ('slug', 'last_mod_time', 'creation_time') + list_display = ('name', 'parent_category', 'index') # 列表页显示字段 + exclude = ('slug', 'last_modify_time', 'creation_time') # 排除自动生成的字段 class LinksAdmin(admin.ModelAdmin): - exclude = ('last_mod_time', 'creation_time') + exclude = ('last_modify_time', 'creation_time') # 排除时间字段 class SideBarAdmin(admin.ModelAdmin): - list_display = ('name', 'content', 'is_enable', 'sequence') - exclude = ('last_mod_time', 'creation_time') + list_display = ('name', 'content', 'is_enable', 'sequence') # 列表页显示字段 + exclude = ('last_modify_time', 'creation_time') # 排除时间字段 class BlogSettingsAdmin(admin.ModelAdmin): - pass + pass # 使用默认的ModelAdmin配置 \ No newline at end of file diff --git a/src/DjangoBlog/blog/apps.py b/src/DjangoBlog/blog/apps.py index 7930587..e363990 100644 --- a/src/DjangoBlog/blog/apps.py +++ b/src/DjangoBlog/blog/apps.py @@ -2,4 +2,4 @@ from django.apps import AppConfig class BlogConfig(AppConfig): - name = 'blog' + name = 'blog' # 指定应用的Python路径为'blog' \ No newline at end of file diff --git a/src/DjangoBlog/blog/context_processors.py b/src/DjangoBlog/blog/context_processors.py index 73e3088..fe376ae 100644 --- a/src/DjangoBlog/blog/context_processors.py +++ b/src/DjangoBlog/blog/context_processors.py @@ -9,35 +9,36 @@ logger = logging.getLogger(__name__) def seo_processor(requests): - key = 'seo_processor' - value = cache.get(key) + # SEO上下文处理器,为模板提供SEO相关变量 + key = 'seo_processor' # 缓存键 + value = cache.get(key) # 尝试从缓存获取数据 if value: - return value + return value # 如果缓存存在,直接返回 else: - logger.info('set processor cache.') - setting = get_blog_setting() + logger.info('set processor cache.') # 记录缓存设置日志 + setting = get_blog_setting() # 获取博客设置 value = { - 'SITE_NAME': setting.site_name, - 'SHOW_GOOGLE_ADSENSE': setting.show_google_adsense, - 'GOOGLE_ADSENSE_CODES': setting.google_adsense_codes, - 'SITE_SEO_DESCRIPTION': setting.site_seo_description, - 'SITE_DESCRIPTION': setting.site_description, - 'SITE_KEYWORDS': setting.site_keywords, - 'SITE_BASE_URL': requests.scheme + '://' + requests.get_host() + '/', - 'ARTICLE_SUB_LENGTH': setting.article_sub_length, - 'nav_category_list': Category.objects.all(), + 'SITE_NAME': setting.site_name, # 网站名称 + 'SHOW_GOOGLE_ADSENSE': setting.show_google_adsense, # 是否显示Google广告 + 'GOOGLE_ADSENSE_CODES': setting.google_adsense_codes, # Google广告代码 + 'SITE_SEO_DESCRIPTION': setting.site_seo_description, # 网站SEO描述 + 'SITE_DESCRIPTION': setting.site_description, # 网站描述 + 'SITE_KEYWORDS': setting.site_keywords, # 网站关键词 + 'SITE_BASE_URL': requests.scheme + '://' + requests.get_host() + '/', # 网站基础URL + 'ARTICLE_SUB_LENGTH': setting.article_sub_length, # 文章摘要长度 + 'nav_category_list': Category.objects.all(), # 导航分类列表 'nav_pages': Article.objects.filter( - type='p', - status='p'), - 'OPEN_SITE_COMMENT': setting.open_site_comment, - 'BEIAN_CODE': setting.beian_code, - 'ANALYTICS_CODE': setting.analytics_code, - "BEIAN_CODE_GONGAN": setting.gongan_beiancode, - "SHOW_GONGAN_CODE": setting.show_gongan_code, - "CURRENT_YEAR": timezone.now().year, - "GLOBAL_HEADER": setting.global_header, - "GLOBAL_FOOTER": setting.global_footer, - "COMMENT_NEED_REVIEW": setting.comment_need_review, + type='p', # 页面类型 + status='p'), # 已发布状态 + 'OPEN_SITE_COMMENT': setting.open_site_comment, # 是否开启网站评论 + 'BEIAN_CODE': setting.beian_code, # 备案号 + 'ANALYTICS_CODE': setting.analytics_code, # 网站统计代码 + "BEIAN_CODE_GONGAN": setting.gongan_beiancode, # 公安备案号 + "SHOW_GONGAN_CODE": setting.show_gongan_code, # 是否显示公安备案 + "CURRENT_YEAR": timezone.now().year, # 当前年份 + "GLOBAL_HEADER": setting.global_header, # 全局头部内容 + "GLOBAL_FOOTER": setting.global_footer, # 全局尾部内容 + "COMMENT_NEED_REVIEW": setting.comment_need_review, # 评论是否需要审核 } - cache.set(key, value, 60 * 60 * 10) - return value + cache.set(key, value, 60 * 60 * 10) # 设置缓存,有效期10小时 + return value # 返回上下文数据 \ No newline at end of file diff --git a/src/DjangoBlog/blog/documents.py b/src/DjangoBlog/blog/documents.py index 0f1db7b..cf26d36 100644 --- a/src/DjangoBlog/blog/documents.py +++ b/src/DjangoBlog/blog/documents.py @@ -1,5 +1,3 @@ -import time - import elasticsearch.client from django.conf import settings from elasticsearch_dsl import Document, InnerDoc, Date, Integer, Long, Text, Object, GeoPoint, Keyword, Boolean @@ -7,19 +5,19 @@ from elasticsearch_dsl.connections import connections from blog.models import Article -ELASTICSEARCH_ENABLED = hasattr(settings, 'ELASTICSEARCH_DSL') +ELASTICSEARCH_ENABLED = hasattr(settings, 'ELASTICSEARCH_DSL') # 检查是否启用Elasticsearch if ELASTICSEARCH_ENABLED: connections.create_connection( - hosts=[settings.ELASTICSEARCH_DSL['default']['hosts']]) + hosts=[settings.ELASTICSEARCH_DSL['default']['hosts']]) # 创建Elasticsearch连接 from elasticsearch import Elasticsearch - es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts']) + es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts']) # Elasticsearch客户端实例 from elasticsearch.client import IngestClient - c = IngestClient(es) + c = IngestClient(es) # Ingest管道客户端 try: - c.get_pipeline('geoip') + c.get_pipeline('geoip') # 尝试获取geoip管道 except elasticsearch.exceptions.NotFoundError: c.put_pipeline('geoip', body='''{ "description" : "Add geoip info", @@ -30,158 +28,172 @@ if ELASTICSEARCH_ENABLED: } } ] - }''') + }''') # 创建geoip处理管道 class GeoIp(InnerDoc): - continent_name = Keyword() - country_iso_code = Keyword() - country_name = Keyword() - location = GeoPoint() + # GeoIP地理位置信息内嵌文档 + continent_name = Keyword() # 大洲名称 + country_iso_code = Keyword() # 国家ISO代码 + country_name = Keyword() # 国家名称 + location = GeoPoint() # 地理位置坐标 class UserAgentBrowser(InnerDoc): - Family = Keyword() - Version = Keyword() + # 用户代理浏览器信息 + Family = Keyword() # 浏览器家族 + Version = Keyword() # 浏览器版本 class UserAgentOS(UserAgentBrowser): + # 用户代理操作系统信息 pass class UserAgentDevice(InnerDoc): - Family = Keyword() - Brand = Keyword() - Model = Keyword() + # 用户代理设备信息 + Family = Keyword() # 设备家族 + Brand = Keyword() # 设备品牌 + Model = Keyword() # 设备型号 class UserAgent(InnerDoc): - browser = Object(UserAgentBrowser, required=False) - os = Object(UserAgentOS, required=False) - device = Object(UserAgentDevice, required=False) - string = Text() - is_bot = Boolean() + # 完整的用户代理信息 + browser = Object(UserAgentBrowser, required=False) # 浏览器对象 + os = Object(UserAgentOS, required=False) # 操作系统对象 + device = Object(UserAgentDevice, required=False) # 设备对象 + string = Text() # 原始用户代理字符串 + is_bot = Boolean() # 是否为机器人 class ElapsedTimeDocument(Document): - url = Keyword() - time_taken = Long() - log_datetime = Date() - ip = Keyword() - geoip = Object(GeoIp, required=False) - useragent = Object(UserAgent, required=False) + # 响应时间文档,用于性能监控 + url = Keyword() # 请求URL + time_taken = Long() # 耗时(毫秒) + log_datetime = Date() # 日志时间 + ip = Keyword() # IP地址 + geoip = Object(GeoIp, required=False) # GeoIP地理位置信息 + useragent = Object(UserAgent, required=False) # 用户代理信息 class Index: - name = 'performance' + name = 'performance' # 索引名称 settings = { - "number_of_shards": 1, - "number_of_replicas": 0 + "number_of_shards": 1, # 分片数量 + "number_of_replicas": 0 # 副本数量 } class Meta: - doc_type = 'ElapsedTime' + doc_type = 'ElapsedTime' # 文档类型 class ElaspedTimeDocumentManager: + # 响应时间文档管理器 @staticmethod def build_index(): + # 构建索引 from elasticsearch import Elasticsearch client = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts']) - res = client.indices.exists(index="performance") + res = client.indices.exists(index="performance") # 检查索引是否存在 if not res: - ElapsedTimeDocument.init() + ElapsedTimeDocument.init() # 初始化索引 @staticmethod def delete_index(): + # 删除索引 from elasticsearch import Elasticsearch es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts']) - es.indices.delete(index='performance', ignore=[400, 404]) + es.indices.delete(index='performance', ignore=[400, 404]) # 忽略404错误 @staticmethod def create(url, time_taken, log_datetime, useragent, ip): - ElaspedTimeDocumentManager.build_index() + # 创建响应时间文档记录 + ElaspedTimeDocumentManager.build_index() # 确保索引存在 ua = UserAgent() ua.browser = UserAgentBrowser() - ua.browser.Family = useragent.browser.family - ua.browser.Version = useragent.browser.version_string + ua.browser.Family = useragent.browser.family # 浏览器家族 + ua.browser.Version = useragent.browser.version_string # 浏览器版本 ua.os = UserAgentOS() - ua.os.Family = useragent.os.family - ua.os.Version = useragent.os.version_string + ua.os.Family = useragent.os.family # 操作系统家族 + ua.os.Version = useragent.os.version_string # 操作系统版本 ua.device = UserAgentDevice() - ua.device.Family = useragent.device.family - ua.device.Brand = useragent.device.brand - ua.device.Model = useragent.device.model - ua.string = useragent.ua_string - ua.is_bot = useragent.is_bot + ua.device.Family = useragent.device.family # 设备家族 + ua.device.Brand = useragent.device.brand # 设备品牌 + ua.device.Model = useragent.device.model # 设备型号 + ua.string = useragent.ua_string # 原始用户代理字符串 + ua.is_bot = useragent.is_bot # 是否为机器人 doc = ElapsedTimeDocument( meta={ 'id': int( round( time.time() * - 1000)) + 1000)) # 使用时间戳作为文档ID }, url=url, time_taken=time_taken, log_datetime=log_datetime, useragent=ua, ip=ip) - doc.save(pipeline="geoip") + doc.save(pipeline="geoip") # 保存文档并使用geoip管道处理 class ArticleDocument(Document): - body = Text(analyzer='ik_max_word', search_analyzer='ik_smart') - title = Text(analyzer='ik_max_word', search_analyzer='ik_smart') + # 文章文档,用于全文搜索 + body = Text(analyzer='ik_max_word', search_analyzer='ik_smart') # 正文,使用IK分词器 + title = Text(analyzer='ik_max_word', search_analyzer='ik_smart') # 标题,使用IK分词器 author = Object(properties={ - 'nickname': Text(analyzer='ik_max_word', search_analyzer='ik_smart'), - 'id': Integer() + 'nickname': Text(analyzer='ik_max_word', search_analyzer='ik_smart'), # 作者昵称 + 'id': Integer() # 作者ID }) category = Object(properties={ - 'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'), - 'id': Integer() + 'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'), # 分类名称 + 'id': Integer() # 分类ID }) tags = Object(properties={ - 'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'), - 'id': Integer() + 'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'), # 标签名称 + 'id': Integer() # 标签ID }) - pub_time = Date() - status = Text() - comment_status = Text() - type = Text() - views = Integer() - article_order = Integer() + pub_time = Date() # 发布时间 + status = Text() # 文章状态 + comment_status = Text() # 评论状态 + type = Text() # 文章类型 + views = Integer() # 浏览量 + article_order = Integer() # 文章排序 class Index: - name = 'blog' + name = 'blog' # 索引名称 settings = { - "number_of_shards": 1, - "number_of_replicas": 0 + "number_of_shards": 1, # 分片数量 + "number_of_replicas": 0 # 副本数量 } class Meta: - doc_type = 'Article' + doc_type = 'Article' # 文档类型 class ArticleDocumentManager(): + # 文章文档管理器 def __init__(self): - self.create_index() + self.create_index() # 初始化时创建索引 def create_index(self): - ArticleDocument.init() + ArticleDocument.init() # 初始化文章索引 def delete_index(self): + # 删除文章索引 from elasticsearch import Elasticsearch es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts']) - es.indices.delete(index='blog', ignore=[400, 404]) + es.indices.delete(index='blog', ignore=[400, 404]) # 忽略404错误 def convert_to_doc(self, articles): + # 将文章模型转换为Elasticsearch文档 return [ ArticleDocument( meta={ - 'id': article.id}, + 'id': article.id}, # 使用文章ID作为文档ID body=article.body, title=article.title, author={ @@ -193,7 +205,7 @@ class ArticleDocumentManager(): tags=[ { 'name': t.name, - 'id': t.id} for t in article.tags.all()], + 'id': t.id} for t in article.tags.all()], # 转换标签列表 pub_time=article.pub_time, status=article.status, comment_status=article.comment_status, @@ -202,12 +214,14 @@ class ArticleDocumentManager(): article_order=article.article_order) for article in articles] def rebuild(self, articles=None): - ArticleDocument.init() - articles = articles if articles else Article.objects.all() - docs = self.convert_to_doc(articles) + # 重建索引 + ArticleDocument.init() # 重新初始化索引 + articles = articles if articles else Article.objects.all() # 获取所有文章或指定文章 + docs = self.convert_to_doc(articles) # 转换为文档 for doc in docs: - doc.save() + doc.save() # 保存所有文档 def update_docs(self, docs): + # 更新文档 for doc in docs: - doc.save() + doc.save() # 保存更新的文档 \ No newline at end of file diff --git a/src/DjangoBlog/blog/forms.py b/src/DjangoBlog/blog/forms.py index 715be76..6d6f5c9 100644 --- a/src/DjangoBlog/blog/forms.py +++ b/src/DjangoBlog/blog/forms.py @@ -7,13 +7,14 @@ logger = logging.getLogger(__name__) class BlogSearchForm(SearchForm): - querydata = forms.CharField(required=True) + querydata = forms.CharField(required=True) # 搜索查询字段,必须填写 def search(self): - datas = super(BlogSearchForm, self).search() - if not self.is_valid(): - return self.no_query_found() + # 重写搜索方法 + datas = super(BlogSearchForm, self).search() # 调用父类的搜索方法 + if not self.is_valid(): # 检查表单是否有效 + return self.no_query_found() # 返回无查询结果 if self.cleaned_data['querydata']: - logger.info(self.cleaned_data['querydata']) - return datas + logger.info(self.cleaned_data['querydata']) # 记录搜索查询日志 + return datas # 返回搜索结果 \ No newline at end of file diff --git a/src/DjangoBlog/blog/management/commands/build_index.py b/src/DjangoBlog/blog/management/commands/build_index.py index 3c4acd7..4b57188 100644 --- a/src/DjangoBlog/blog/management/commands/build_index.py +++ b/src/DjangoBlog/blog/management/commands/build_index.py @@ -6,13 +6,14 @@ from blog.documents import ElapsedTimeDocument, ArticleDocumentManager, ElaspedT # TODO 参数化 class Command(BaseCommand): - help = 'build search index' + help = 'build search index' # 命令帮助信息 def handle(self, *args, **options): - if ELASTICSEARCH_ENABLED: - ElaspedTimeDocumentManager.build_index() - manager = ElapsedTimeDocument() - manager.init() - manager = ArticleDocumentManager() - manager.delete_index() - manager.rebuild() + # 处理命令的主方法 + if ELASTICSEARCH_ENABLED: # 检查Elasticsearch是否启用 + ElaspedTimeDocumentManager.build_index() # 构建耗时文档索引 + manager = ElapsedTimeDocument() # 创建耗时文档管理器实例 + manager.init() # 初始化耗时文档 + manager = ArticleDocumentManager() # 创建文章文档管理器实例 + manager.delete_index() # 删除现有文章索引 + manager.rebuild() # 重新构建文章索引 \ No newline at end of file diff --git a/src/DjangoBlog/blog/management/commands/build_search_words.py b/src/DjangoBlog/blog/management/commands/build_search_words.py index cfe7e0d..f9d1e99 100644 --- a/src/DjangoBlog/blog/management/commands/build_search_words.py +++ b/src/DjangoBlog/blog/management/commands/build_search_words.py @@ -5,9 +5,12 @@ from blog.models import Tag, Category # TODO 参数化 class Command(BaseCommand): - help = 'build search words' + help = 'build search words' # 命令帮助信息:构建搜索词 def handle(self, *args, **options): + # 处理命令的主方法 + # 从标签和分类中获取所有名称,使用集合去重 datas = set([t.name for t in Tag.objects.all()] + [t.name for t in Category.objects.all()]) - print('\n'.join(datas)) + # 将去重后的数据按行打印输出 + print('\n'.join(datas)) \ No newline at end of file diff --git a/src/DjangoBlog/blog/management/commands/clear_cache.py b/src/DjangoBlog/blog/management/commands/clear_cache.py index 0d66172..09f8c93 100644 --- a/src/DjangoBlog/blog/management/commands/clear_cache.py +++ b/src/DjangoBlog/blog/management/commands/clear_cache.py @@ -4,8 +4,9 @@ from djangoblog.utils import cache class Command(BaseCommand): - help = 'clear the whole cache' + help = 'clear the whole cache' # 命令帮助信息:清除整个缓存 def handle(self, *args, **options): - cache.clear() - self.stdout.write(self.style.SUCCESS('Cleared cache\n')) + # 处理命令的主方法 + cache.clear() # 清除所有缓存 + self.stdout.write(self.style.SUCCESS('Cleared cache\n')) # 输出成功信息 \ No newline at end of file diff --git a/src/DjangoBlog/blog/management/commands/create_testdata.py b/src/DjangoBlog/blog/management/commands/create_testdata.py index 675d2ba..7c79b2a 100644 --- a/src/DjangoBlog/blog/management/commands/create_testdata.py +++ b/src/DjangoBlog/blog/management/commands/create_testdata.py @@ -6,35 +6,44 @@ from blog.models import Article, Tag, Category class Command(BaseCommand): - help = 'create test datas' + help = 'create test datas' # 命令帮助信息:创建测试数据 def handle(self, *args, **options): + # 处理命令的主方法 + # 获取或创建测试用户 user = get_user_model().objects.get_or_create( email='test@test.com', username='测试用户', password=make_password('test!q@w#eTYU'))[0] + # 获取或创建父类目 pcategory = Category.objects.get_or_create( name='我是父类目', parent_category=None)[0] + # 获取或创建子类目 category = Category.objects.get_or_create( name='子类目', parent_category=pcategory)[0] - category.save() + category.save() # 保存子类目 + # 创建基础标签 basetag = Tag() basetag.name = "标签" basetag.save() + # 循环创建测试文章 for i in range(1, 20): + # 获取或创建文章 article = Article.objects.get_or_create( category=category, title='nice title ' + str(i), body='nice content ' + str(i), author=user)[0] + # 创建文章专属标签 tag = Tag() tag.name = "标签" + str(i) tag.save() + # 为文章添加标签 article.tags.add(tag) article.tags.add(basetag) - article.save() + article.save() # 保存文章 from djangoblog.utils import cache - cache.clear() - self.stdout.write(self.style.SUCCESS('created test datas \n')) + cache.clear() # 清除缓存 + self.stdout.write(self.style.SUCCESS('created test datas \n')) # 输出成功信息 \ No newline at end of file diff --git a/src/DjangoBlog/blog/management/commands/ping_baidu.py b/src/DjangoBlog/blog/management/commands/ping_baidu.py index 2c7fbdd..6bf82fb 100644 --- a/src/DjangoBlog/blog/management/commands/ping_baidu.py +++ b/src/DjangoBlog/blog/management/commands/ping_baidu.py @@ -4,47 +4,52 @@ from djangoblog.spider_notify import SpiderNotify from djangoblog.utils import get_current_site from blog.models import Article, Tag, Category -site = get_current_site().domain +site = get_current_site().domain # 获取当前站点域名 class Command(BaseCommand): - help = 'notify baidu url' + help = 'notify baidu url' # 命令帮助信息:通知百度URL def add_arguments(self, parser): + # 添加命令行参数 parser.add_argument( - 'data_type', - type=str, + 'data_type', # 参数名 + type=str, # 参数类型 choices=[ 'all', 'article', 'tag', - 'category'], - help='article : all article,tag : all tag,category: all category,all: All of these') + 'category'], # 参数可选值 + help='article : all article,tag : all tag,category: all category,all: All of these') # 参数帮助信息 def get_full_url(self, path): + # 根据相对路径构建完整URL url = "https://{site}{path}".format(site=site, path=path) return url def handle(self, *args, **options): - type = options['data_type'] - self.stdout.write('start get %s' % type) + type = options['data_type'] # 获取数据类型参数 + self.stdout.write('start get %s' % type) # 输出开始信息 - urls = [] + urls = [] # 初始化URL列表 + # 根据类型收集文章URL if type == 'article' or type == 'all': - for article in Article.objects.filter(status='p'): - urls.append(article.get_full_url()) + for article in Article.objects.filter(status='p'): # 只获取已发布的文章 + urls.append(article.get_full_url()) # 添加文章完整URL + # 根据类型收集标签URL if type == 'tag' or type == 'all': for tag in Tag.objects.all(): - url = tag.get_absolute_url() - urls.append(self.get_full_url(url)) + url = tag.get_absolute_url() # 获取标签相对URL + urls.append(self.get_full_url(url)) # 添加标签完整URL + # 根据类型收集分类URL if type == 'category' or type == 'all': for category in Category.objects.all(): - url = category.get_absolute_url() - urls.append(self.get_full_url(url)) + url = category.get_absolute_url() # 获取分类相对URL + urls.append(self.get_full_url(url)) # 添加分类完整URL self.stdout.write( self.style.SUCCESS( 'start notify %d urls' % - len(urls))) - SpiderNotify.baidu_notify(urls) - self.stdout.write(self.style.SUCCESS('finish notify')) + len(urls))) # 输出开始通知信息 + SpiderNotify.baidu_notify(urls) # 调用百度站长平台URL推送 + self.stdout.write(self.style.SUCCESS('finish notify')) # 输出完成信息 \ No newline at end of file diff --git a/src/DjangoBlog/blog/management/commands/sync_user_avatar.py b/src/DjangoBlog/blog/management/commands/sync_user_avatar.py index d0f4612..22608ae 100644 --- a/src/DjangoBlog/blog/management/commands/sync_user_avatar.py +++ b/src/DjangoBlog/blog/management/commands/sync_user_avatar.py @@ -8,40 +8,41 @@ from oauth.oauthmanager import get_manager_by_type class Command(BaseCommand): - help = 'sync user avatar' + help = 'sync user avatar' # 命令帮助信息:同步用户头像 def test_picture(self, url): + # 测试图片URL是否可访问 try: - if requests.get(url, timeout=2).status_code == 200: - return True + if requests.get(url, timeout=2).status_code == 200: # 发送HTTP请求测试图片 + return True # 图片可访问返回True except: - pass + pass # 发生异常时静默处理 def handle(self, *args, **options): - static_url = static("../") - users = OAuthUser.objects.all() - self.stdout.write(f'开始同步{len(users)}个用户头像') + static_url = static("../") # 获取静态文件基础URL + users = OAuthUser.objects.all() # 获取所有OAuth用户 + self.stdout.write(f'开始同步{len(users)}个用户头像') # 输出开始同步信息 for u in users: - self.stdout.write(f'开始同步:{u.nickname}') - url = u.picture + self.stdout.write(f'开始同步:{u.nickname}') # 输出当前同步的用户 + url = u.picture # 获取用户当前头像URL if url: - if url.startswith(static_url): - if self.test_picture(url): - continue + if url.startswith(static_url): # 检查是否已经是静态文件URL + if self.test_picture(url): # 测试静态图片是否可访问 + continue # 可访问则跳过处理 else: - if u.metadata: - manage = get_manager_by_type(u.type) - url = manage.get_picture(u.metadata) - url = save_user_avatar(url) + if u.metadata: # 如果有用户元数据 + manage = get_manager_by_type(u.type) # 根据OAuth类型获取管理器 + url = manage.get_picture(u.metadata) # 从OAuth平台获取头像URL + url = save_user_avatar(url) # 保存头像并返回新URL else: - url = static('blog/img/avatar.png') + url = static('blog/img/avatar.png') # 使用默认头像 else: - url = save_user_avatar(url) + url = save_user_avatar(url) # 保存外部头像并返回新URL else: - url = static('blog/img/avatar.png') + url = static('blog/img/avatar.png') # 没有头像时使用默认头像 if url: self.stdout.write( - f'结束同步:{u.nickname}.url:{url}') - u.picture = url - u.save() - self.stdout.write('结束同步') + f'结束同步:{u.nickname}.url:{url}') # 输出同步完成信息 + u.picture = url # 更新用户头像URL + u.save() # 保存用户信息 + self.stdout.write('结束同步') # 输出同步结束信息 \ No newline at end of file diff --git a/src/DjangoBlog/blog/middleware.py b/src/DjangoBlog/blog/middleware.py index 94dd70c..58a07e2 100644 --- a/src/DjangoBlog/blog/middleware.py +++ b/src/DjangoBlog/blog/middleware.py @@ -11,32 +11,32 @@ logger = logging.getLogger(__name__) class OnlineMiddleware(object): def __init__(self, get_response=None): - self.get_response = get_response + self.get_response = get_response # 获取响应的方法 super().__init__() def __call__(self, request): - ''' page render time ''' - start_time = time.time() - response = self.get_response(request) - http_user_agent = request.META.get('HTTP_USER_AGENT', '') - ip, _ = get_client_ip(request) - user_agent = parse(http_user_agent) - if not response.streaming: + ''' page render time ''' # 页面渲染时间统计中间件 + start_time = time.time() # 记录开始时间 + response = self.get_response(request) # 获取响应 + http_user_agent = request.META.get('HTTP_USER_AGENT', '') # 获取用户代理 + ip, _ = get_client_ip(request) # 获取客户端IP + user_agent = parse(http_user_agent) # 解析用户代理 + if not response.streaming: # 如果不是流式响应 try: - cast_time = time.time() - start_time - if ELASTICSEARCH_ENABLED: - time_taken = round((cast_time) * 1000, 2) - url = request.path + cast_time = time.time() - start_time # 计算耗时 + if ELASTICSEARCH_ENABLED: # 如果启用了Elasticsearch + time_taken = round((cast_time) * 1000, 2) # 转换为毫秒并保留两位小数 + url = request.path # 获取请求路径 from django.utils import timezone ElaspedTimeDocumentManager.create( url=url, time_taken=time_taken, - log_datetime=timezone.now(), + log_datetime=timezone.now(), # 当前时间 useragent=user_agent, - ip=ip) + ip=ip) # 创建性能监控记录 response.content = response.content.replace( - b'', str.encode(str(cast_time)[:5])) + b'', str.encode(str(cast_time)[:5])) # 替换加载时间占位符 except Exception as e: - logger.error("Error OnlineMiddleware: %s" % e) + logger.error("Error OnlineMiddleware: %s" % e) # 记录错误日志 - return response + return response # 返回响应 \ No newline at end of file diff --git a/src/DjangoBlog/blog/migrations/0001_initial.py b/src/DjangoBlog/blog/migrations/0001_initial.py index 3d391b6..1e33130 100644 --- a/src/DjangoBlog/blog/migrations/0001_initial.py +++ b/src/DjangoBlog/blog/migrations/0001_initial.py @@ -9,13 +9,14 @@ import mdeditor.fields class Migration(migrations.Migration): - initial = True + initial = True # 标记为初始迁移 dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), # 依赖用户模型 ] operations = [ + # 创建网站配置表 migrations.CreateModel( name='BlogSettings', fields=[ @@ -37,10 +38,11 @@ class Migration(migrations.Migration): ('gongan_beiancode', models.TextField(blank=True, default='', max_length=2000, null=True, verbose_name='公安备案号')), ], options={ - 'verbose_name': '网站配置', - 'verbose_name_plural': '网站配置', + 'verbose_name': '网站配置', # 单数显示名称 + 'verbose_name_plural': '网站配置', # 复数显示名称 }, ), + # 创建友情链接表 migrations.CreateModel( name='Links', fields=[ @@ -56,9 +58,10 @@ class Migration(migrations.Migration): options={ 'verbose_name': '友情链接', 'verbose_name_plural': '友情链接', - 'ordering': ['sequence'], + 'ordering': ['sequence'], # 按排序字段升序排列 }, ), + # 创建侧边栏表 migrations.CreateModel( name='SideBar', fields=[ @@ -73,49 +76,52 @@ class Migration(migrations.Migration): options={ 'verbose_name': '侧边栏', 'verbose_name_plural': '侧边栏', - 'ordering': ['sequence'], + 'ordering': ['sequence'], # 按排序字段升序排列 }, ), + # 创建标签表 migrations.CreateModel( name='Tag', fields=[ - ('id', models.AutoField(primary_key=True, serialize=False)), + ('id', models.AutoField(primary_key=True, serialize=False)), # 自增主键 ('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')), ('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')), ('name', models.CharField(max_length=30, unique=True, verbose_name='标签名')), - ('slug', models.SlugField(blank=True, default='no-slug', max_length=60)), + ('slug', models.SlugField(blank=True, default='no-slug', max_length=60)), # URL友好标识 ], options={ 'verbose_name': '标签', 'verbose_name_plural': '标签', - 'ordering': ['name'], + 'ordering': ['name'], # 按名称升序排列 }, ), + # 创建分类表 migrations.CreateModel( name='Category', fields=[ - ('id', models.AutoField(primary_key=True, serialize=False)), + ('id', models.AutoField(primary_key=True, serialize=False)), # 自增主键 ('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')), ('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')), ('name', models.CharField(max_length=30, unique=True, verbose_name='分类名')), - ('slug', models.SlugField(blank=True, default='no-slug', max_length=60)), + ('slug', models.SlugField(blank=True, default='no-slug', max_length=60)), # URL友好标识 ('index', models.IntegerField(default=0, verbose_name='权重排序-越大越靠前')), - ('parent_category', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='父级分类')), + ('parent_category', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='父级分类')), # 自关联外键 ], options={ 'verbose_name': '分类', 'verbose_name_plural': '分类', - 'ordering': ['-index'], + 'ordering': ['-index'], # 按权重倒序排列 }, ), + # 创建文章表 migrations.CreateModel( name='Article', fields=[ - ('id', models.AutoField(primary_key=True, serialize=False)), + ('id', models.AutoField(primary_key=True, serialize=False)), # 自增主键 ('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')), ('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')), ('title', models.CharField(max_length=200, unique=True, verbose_name='标题')), - ('body', mdeditor.fields.MDTextField(verbose_name='正文')), + ('body', mdeditor.fields.MDTextField(verbose_name='正文')), # Markdown编辑器字段 ('pub_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='发布时间')), ('status', models.CharField(choices=[('d', '草稿'), ('p', '发表')], default='p', max_length=1, verbose_name='文章状态')), ('comment_status', models.CharField(choices=[('o', '打开'), ('c', '关闭')], default='o', max_length=1, verbose_name='评论状态')), @@ -123,15 +129,15 @@ class Migration(migrations.Migration): ('views', models.PositiveIntegerField(default=0, verbose_name='浏览量')), ('article_order', models.IntegerField(default=0, verbose_name='排序,数字越大越靠前')), ('show_toc', models.BooleanField(default=False, verbose_name='是否显示toc目录')), - ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='作者')), - ('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='分类')), - ('tags', models.ManyToManyField(blank=True, to='blog.tag', verbose_name='标签集合')), + ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='作者')), # 关联用户模型 + ('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='分类')), # 关联分类模型 + ('tags', models.ManyToManyField(blank=True, to='blog.tag', verbose_name='标签集合')), # 多对多关联标签 ], options={ 'verbose_name': '文章', 'verbose_name_plural': '文章', - 'ordering': ['-article_order', '-pub_time'], - 'get_latest_by': 'id', + 'ordering': ['-article_order', '-pub_time'], # 按文章排序和发布时间倒序排列 + 'get_latest_by': 'id', # 指定获取最新记录的字段 }, ), - ] + ] \ No newline at end of file diff --git a/src/DjangoBlog/blog/migrations/0002_blogsettings_global_footer_and_more.py b/src/DjangoBlog/blog/migrations/0002_blogsettings_global_footer_and_more.py index adbaa36..c943792 100644 --- a/src/DjangoBlog/blog/migrations/0002_blogsettings_global_footer_and_more.py +++ b/src/DjangoBlog/blog/migrations/0002_blogsettings_global_footer_and_more.py @@ -6,18 +6,20 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('blog', '0001_initial'), + ('blog', '0001_initial'), # 依赖blog应用的初始迁移 ] operations = [ + # 向BlogSettings模型添加global_footer字段 migrations.AddField( model_name='blogsettings', name='global_footer', field=models.TextField(blank=True, default='', null=True, verbose_name='公共尾部'), ), + # 向BlogSettings模型添加global_header字段 migrations.AddField( model_name='blogsettings', name='global_header', field=models.TextField(blank=True, default='', null=True, verbose_name='公共头部'), ), - ] + ] \ No newline at end of file diff --git a/src/DjangoBlog/blog/migrations/0003_blogsettings_comment_need_review.py b/src/DjangoBlog/blog/migrations/0003_blogsettings_comment_need_review.py index e9f5502..9830c6b 100644 --- a/src/DjangoBlog/blog/migrations/0003_blogsettings_comment_need_review.py +++ b/src/DjangoBlog/blog/migrations/0003_blogsettings_comment_need_review.py @@ -5,13 +5,14 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('blog', '0002_blogsettings_global_footer_and_more'), + ('blog', '0002_blogsettings_global_footer_and_more'), # 依赖blog应用的第二个迁移 ] operations = [ + # 向BlogSettings模型添加comment_need_review字段 migrations.AddField( model_name='blogsettings', name='comment_need_review', field=models.BooleanField(default=False, verbose_name='评论是否需要审核'), ), - ] + ] \ No newline at end of file diff --git a/src/DjangoBlog/blog/migrations/0004_rename_analyticscode_blogsettings_analytics_code_and_more.py b/src/DjangoBlog/blog/migrations/0004_rename_analyticscode_blogsettings_analytics_code_and_more.py index ceb1398..f33876d 100644 --- a/src/DjangoBlog/blog/migrations/0004_rename_analyticscode_blogsettings_analytics_code_and_more.py +++ b/src/DjangoBlog/blog/migrations/0004_rename_analyticscode_blogsettings_analytics_code_and_more.py @@ -5,23 +5,26 @@ from django.db import migrations class Migration(migrations.Migration): dependencies = [ - ('blog', '0003_blogsettings_comment_need_review'), + ('blog', '0003_blogsettings_comment_need_review'), # 依赖blog应用的第三个迁移 ] operations = [ + # 重命名字段:analyticscode -> analytics_code migrations.RenameField( model_name='blogsettings', old_name='analyticscode', new_name='analytics_code', ), + # 重命名字段:beiancode -> beian_code migrations.RenameField( model_name='blogsettings', old_name='beiancode', new_name='beian_code', ), + # 重命名字段:sitename -> site_name migrations.RenameField( model_name='blogsettings', old_name='sitename', new_name='site_name', ), - ] + ] \ No newline at end of file diff --git a/src/DjangoBlog/blog/migrations/0005_alter_article_options_alter_category_options_and_more.py b/src/DjangoBlog/blog/migrations/0005_alter_article_options_alter_category_options_and_more.py index d08e853..be2274d 100644 --- a/src/DjangoBlog/blog/migrations/0005_alter_article_options_alter_category_options_and_more.py +++ b/src/DjangoBlog/blog/migrations/0005_alter_article_options_alter_category_options_and_more.py @@ -11,290 +11,349 @@ class Migration(migrations.Migration): dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('blog', '0004_rename_analyticscode_blogsettings_analytics_code_and_more'), + ('blog', '0004_rename_analyticscode_blogsettings_analytics_code_and_more'), # 依赖blog应用的第四个迁移 ] operations = [ + # 修改Article模型的元数据选项(国际化) migrations.AlterModelOptions( name='article', options={'get_latest_by': 'id', 'ordering': ['-article_order', '-pub_time'], 'verbose_name': 'article', 'verbose_name_plural': 'article'}, ), + # 修改Category模型的元数据选项(国际化) migrations.AlterModelOptions( name='category', options={'ordering': ['-index'], 'verbose_name': 'category', 'verbose_name_plural': 'category'}, ), + # 修改Links模型的元数据选项(国际化) migrations.AlterModelOptions( name='links', options={'ordering': ['sequence'], 'verbose_name': 'link', 'verbose_name_plural': 'link'}, ), + # 修改Sidebar模型的元数据选项(国际化) migrations.AlterModelOptions( name='sidebar', options={'ordering': ['sequence'], 'verbose_name': 'sidebar', 'verbose_name_plural': 'sidebar'}, ), + # 修改Tag模型的元数据选项(国际化) migrations.AlterModelOptions( name='tag', options={'ordering': ['name'], 'verbose_name': 'tag', 'verbose_name_plural': 'tag'}, ), + # 删除Article模型的created_time字段 migrations.RemoveField( model_name='article', name='created_time', ), + # 删除Article模型的last_mod_time字段 migrations.RemoveField( model_name='article', name='last_mod_time', ), + # 删除Category模型的created_time字段 migrations.RemoveField( model_name='category', name='created_time', ), + # 删除Category模型的last_mod_time字段 migrations.RemoveField( model_name='category', name='last_mod_time', ), + # 删除Links模型的created_time字段 migrations.RemoveField( model_name='links', name='created_time', ), + # 删除Sidebar模型的created_time字段 migrations.RemoveField( model_name='sidebar', name='created_time', ), + # 删除Tag模型的created_time字段 migrations.RemoveField( model_name='tag', name='created_time', ), + # 删除Tag模型的last_mod_time字段 migrations.RemoveField( model_name='tag', name='last_mod_time', ), + # 向Article模型添加creation_time字段 migrations.AddField( model_name='article', name='creation_time', field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'), ), + # 向Article模型添加last_modify_time字段 migrations.AddField( model_name='article', name='last_modify_time', field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'), ), + # 向Category模型添加creation_time字段 migrations.AddField( model_name='category', name='creation_time', field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'), ), + # 向Category模型添加last_modify_time字段 migrations.AddField( model_name='category', name='last_modify_time', field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'), ), + # 向Links模型添加creation_time字段 migrations.AddField( model_name='links', name='creation_time', field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'), ), + # 向Sidebar模型添加creation_time字段 migrations.AddField( model_name='sidebar', name='creation_time', field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'), ), + # 向Tag模型添加creation_time字段 migrations.AddField( model_name='tag', name='creation_time', field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'), ), + # 向Tag模型添加last_modify_time字段 migrations.AddField( model_name='tag', name='last_modify_time', field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'), ), + # 修改Article模型的article_order字段verbose_name(国际化) migrations.AlterField( model_name='article', name='article_order', field=models.IntegerField(default=0, verbose_name='order'), ), + # 修改Article模型的author字段verbose_name(国际化) migrations.AlterField( model_name='article', name='author', field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='author'), ), + # 修改Article模型的body字段verbose_name(国际化) migrations.AlterField( model_name='article', name='body', field=mdeditor.fields.MDTextField(verbose_name='body'), ), + # 修改Article模型的category字段verbose_name(国际化) migrations.AlterField( model_name='article', name='category', field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='category'), ), + # 修改Article模型的comment_status字段verbose_name和选项(国际化) migrations.AlterField( model_name='article', name='comment_status', field=models.CharField(choices=[('o', 'Open'), ('c', 'Close')], default='o', max_length=1, verbose_name='comment status'), ), + # 修改Article模型的pub_time字段verbose_name(国际化) migrations.AlterField( model_name='article', name='pub_time', field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='publish time'), ), + # 修改Article模型的show_toc字段verbose_name(国际化) migrations.AlterField( model_name='article', name='show_toc', field=models.BooleanField(default=False, verbose_name='show toc'), ), + # 修改Article模型的status字段verbose_name和选项(国际化) migrations.AlterField( model_name='article', name='status', field=models.CharField(choices=[('d', 'Draft'), ('p', 'Published')], default='p', max_length=1, verbose_name='status'), ), + # 修改Article模型的tags字段verbose_name(国际化) migrations.AlterField( model_name='article', name='tags', field=models.ManyToManyField(blank=True, to='blog.tag', verbose_name='tag'), ), + # 修改Article模型的title字段verbose_name(国际化) migrations.AlterField( model_name='article', name='title', field=models.CharField(max_length=200, unique=True, verbose_name='title'), ), + # 修改Article模型的type字段verbose_name和选项(国际化) migrations.AlterField( model_name='article', name='type', field=models.CharField(choices=[('a', 'Article'), ('p', 'Page')], default='a', max_length=1, verbose_name='type'), ), + # 修改Article模型的views字段verbose_name(国际化) migrations.AlterField( model_name='article', name='views', field=models.PositiveIntegerField(default=0, verbose_name='views'), ), + # 修改BlogSettings模型的article_comment_count字段verbose_name(国际化) migrations.AlterField( model_name='blogsettings', name='article_comment_count', field=models.IntegerField(default=5, verbose_name='article comment count'), ), + # 修改BlogSettings模型的article_sub_length字段verbose_name(国际化) migrations.AlterField( model_name='blogsettings', name='article_sub_length', field=models.IntegerField(default=300, verbose_name='article sub length'), ), + # 修改BlogSettings模型的google_adsense_codes字段verbose_name(国际化) migrations.AlterField( model_name='blogsettings', name='google_adsense_codes', field=models.TextField(blank=True, default='', max_length=2000, null=True, verbose_name='adsense code'), ), + # 修改BlogSettings模型的open_site_comment字段verbose_name(国际化) migrations.AlterField( model_name='blogsettings', name='open_site_comment', field=models.BooleanField(default=True, verbose_name='open site comment'), ), + # 修改BlogSettings模型的show_google_adsense字段verbose_name(国际化) migrations.AlterField( model_name='blogsettings', name='show_google_adsense', field=models.BooleanField(default=False, verbose_name='show adsense'), ), + # 修改BlogSettings模型的sidebar_article_count字段verbose_name(国际化) migrations.AlterField( model_name='blogsettings', name='sidebar_article_count', field=models.IntegerField(default=10, verbose_name='sidebar article count'), ), + # 修改BlogSettings模型的sidebar_comment_count字段verbose_name(国际化) migrations.AlterField( model_name='blogsettings', name='sidebar_comment_count', field=models.IntegerField(default=5, verbose_name='sidebar comment count'), ), + # 修改BlogSettings模型的site_description字段verbose_name(国际化) migrations.AlterField( model_name='blogsettings', name='site_description', field=models.TextField(default='', max_length=1000, verbose_name='site description'), ), + # 修改BlogSettings模型的site_keywords字段verbose_name(国际化) migrations.AlterField( model_name='blogsettings', name='site_keywords', field=models.TextField(default='', max_length=1000, verbose_name='site keywords'), ), + # 修改BlogSettings模型的site_name字段verbose_name(国际化) migrations.AlterField( model_name='blogsettings', name='site_name', field=models.CharField(default='', max_length=200, verbose_name='site name'), ), + # 修改BlogSettings模型的site_seo_description字段verbose_name(国际化) migrations.AlterField( model_name='blogsettings', name='site_seo_description', field=models.TextField(default='', max_length=1000, verbose_name='site seo description'), ), + # 修改Category模型的index字段verbose_name(国际化) migrations.AlterField( model_name='category', name='index', field=models.IntegerField(default=0, verbose_name='index'), ), + # 修改Category模型的name字段verbose_name(国际化) migrations.AlterField( model_name='category', name='name', field=models.CharField(max_length=30, unique=True, verbose_name='category name'), ), + # 修改Category模型的parent_category字段verbose_name(国际化) migrations.AlterField( model_name='category', name='parent_category', field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='parent category'), ), + # 修改Links模型的is_enable字段verbose_name(国际化) migrations.AlterField( model_name='links', name='is_enable', field=models.BooleanField(default=True, verbose_name='is show'), ), + # 修改Links模型的last_mod_time字段verbose_name(国际化) migrations.AlterField( model_name='links', name='last_mod_time', field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'), ), + # 修改Links模型的link字段verbose_name(国际化) migrations.AlterField( model_name='links', name='link', field=models.URLField(verbose_name='link'), ), + # 修改Links模型的name字段verbose_name(国际化) migrations.AlterField( model_name='links', name='name', field=models.CharField(max_length=30, unique=True, verbose_name='link name'), ), + # 修改Links模型的sequence字段verbose_name(国际化) migrations.AlterField( model_name='links', name='sequence', field=models.IntegerField(unique=True, verbose_name='order'), ), + # 修改Links模型的show_type字段verbose_name和选项(国际化) migrations.AlterField( model_name='links', name='show_type', field=models.CharField(choices=[('i', 'index'), ('l', 'list'), ('p', 'post'), ('a', 'all'), ('s', 'slide')], default='i', max_length=1, verbose_name='show type'), ), + # 修改Sidebar模型的content字段verbose_name(国际化) migrations.AlterField( model_name='sidebar', name='content', field=models.TextField(verbose_name='content'), ), + # 修改Sidebar模型的is_enable字段verbose_name(国际化) migrations.AlterField( model_name='sidebar', name='is_enable', field=models.BooleanField(default=True, verbose_name='is enable'), ), + # 修改Sidebar模型的last_mod_time字段verbose_name(国际化) migrations.AlterField( model_name='sidebar', name='last_mod_time', field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'), ), + # 修改Sidebar模型的name字段verbose_name(国际化) migrations.AlterField( model_name='sidebar', name='name', field=models.CharField(max_length=100, verbose_name='title'), ), + # 修改Sidebar模型的sequence字段verbose_name(国际化) migrations.AlterField( model_name='sidebar', name='sequence', field=models.IntegerField(unique=True, verbose_name='order'), ), + # 修改Tag模型的name字段verbose_name(国际化) migrations.AlterField( model_name='tag', name='name', field=models.CharField(max_length=30, unique=True, verbose_name='tag name'), ), - ] + ] \ No newline at end of file diff --git a/src/DjangoBlog/blog/migrations/0006_alter_blogsettings_options.py b/src/DjangoBlog/blog/migrations/0006_alter_blogsettings_options.py index e36feb4..81ecef6 100644 --- a/src/DjangoBlog/blog/migrations/0006_alter_blogsettings_options.py +++ b/src/DjangoBlog/blog/migrations/0006_alter_blogsettings_options.py @@ -8,7 +8,22 @@ class Migration(migrations.Migration): dependencies = [ ('blog', '0005_alter_article_options_alter_category_options_and_more'), ] + # Generated by Django 4.2.7 on 2024-01-26 02:41 + from django.db import migrations + + class Migration(migrations.Migration): + dependencies = [ + ('blog', '0005_alter_article_options_alter_category_options_and_more'), # 依赖blog应用的第五个迁移 + ] + + operations = [ + # 修改BlogSettings模型的元数据选项(国际化) + migrations.AlterModelOptions( + name='blogsettings', + options={'verbose_name': 'Website configuration', 'verbose_name_plural': 'Website configuration'}, + ), + ] operations = [ migrations.AlterModelOptions( name='blogsettings', diff --git a/src/DjangoBlog/blog/models.py b/src/DjangoBlog/blog/models.py index 083788b..a2bac48 100644 --- a/src/DjangoBlog/blog/models.py +++ b/src/DjangoBlog/blog/models.py @@ -18,106 +18,114 @@ logger = logging.getLogger(__name__) class LinkShowType(models.TextChoices): - I = ('i', _('index')) - L = ('l', _('list')) - P = ('p', _('post')) - A = ('a', _('all')) - S = ('s', _('slide')) + # 链接显示类型选择 + I = ('i', _('index')) # 首页显示 + L = ('l', _('list')) # 列表页显示 + P = ('p', _('post')) # 文章页面显示 + A = ('a', _('all')) # 全站显示 + S = ('s', _('slide')) # 幻灯片显示 class BaseModel(models.Model): - id = models.AutoField(primary_key=True) - creation_time = models.DateTimeField(_('creation time'), default=now) - last_modify_time = models.DateTimeField(_('modify time'), default=now) + # 基础模型类,提供公共字段和方法 + id = models.AutoField(primary_key=True) # 自增主键 + creation_time = models.DateTimeField(_('creation time'), default=now) # 创建时间 + last_modify_time = models.DateTimeField(_('modify time'), default=now) # 最后修改时间 def save(self, *args, **kwargs): + # 重写保存方法,处理特殊逻辑 is_update_views = isinstance( self, - Article) and 'update_fields' in kwargs and kwargs['update_fields'] == ['views'] + Article) and 'update_fields' in kwargs and kwargs['update_fields'] == ['views'] # 检查是否为更新浏览量 if is_update_views: - Article.objects.filter(pk=self.pk).update(views=self.views) + Article.objects.filter(pk=self.pk).update(views=self.views) # 直接更新浏览量,避免触发其他逻辑 else: - if 'slug' in self.__dict__: + if 'slug' in self.__dict__: # 如果有slug字段 slug = getattr( self, 'title') if 'title' in self.__dict__ else getattr( - self, 'name') - setattr(self, 'slug', slugify(slug)) - super().save(*args, **kwargs) + self, 'name') # 根据title或name生成slug + setattr(self, 'slug', slugify(slug)) # 设置slug值 + super().save(*args, **kwargs) # 调用父类保存方法 def get_full_url(self): - site = get_current_site().domain + # 获取完整URL + site = get_current_site().domain # 获取当前站点域名 url = "https://{site}{path}".format(site=site, - path=self.get_absolute_url()) + path=self.get_absolute_url()) # 构建完整URL return url class Meta: - abstract = True + abstract = True # 抽象基类,不会创建数据库表 @abstractmethod def get_absolute_url(self): + # 抽象方法,子类必须实现获取绝对URL的方法 pass class Article(BaseModel): - """文章""" + """文章模型""" STATUS_CHOICES = ( - ('d', _('Draft')), - ('p', _('Published')), + ('d', _('Draft')), # 草稿状态 + ('p', _('Published')), # 已发布状态 ) COMMENT_STATUS = ( - ('o', _('Open')), - ('c', _('Close')), + ('o', _('Open')), # 评论开启 + ('c', _('Close')), # 评论关闭 ) TYPE = ( - ('a', _('Article')), - ('p', _('Page')), + ('a', _('Article')), # 文章类型 + ('p', _('Page')), # 页面类型 ) - title = models.CharField(_('title'), max_length=200, unique=True) - body = MDTextField(_('body')) + title = models.CharField(_('title'), max_length=200, unique=True) # 文章标题 + body = MDTextField(_('body')) # 文章正文,使用Markdown编辑器 pub_time = models.DateTimeField( - _('publish time'), blank=False, null=False, default=now) + _('publish time'), blank=False, null=False, default=now) # 发布时间 status = models.CharField( _('status'), max_length=1, choices=STATUS_CHOICES, - default='p') + default='p') # 文章状态 comment_status = models.CharField( _('comment status'), max_length=1, choices=COMMENT_STATUS, - default='o') - type = models.CharField(_('type'), max_length=1, choices=TYPE, default='a') - views = models.PositiveIntegerField(_('views'), default=0) + default='o') # 评论状态 + type = models.CharField(_('type'), max_length=1, choices=TYPE, default='a') # 文章类型 + views = models.PositiveIntegerField(_('views'), default=0) # 浏览量 author = models.ForeignKey( settings.AUTH_USER_MODEL, verbose_name=_('author'), blank=False, null=False, - on_delete=models.CASCADE) + on_delete=models.CASCADE) # 作者外键 article_order = models.IntegerField( - _('order'), blank=False, null=False, default=0) - show_toc = models.BooleanField(_('show toc'), blank=False, null=False, default=False) + _('order'), blank=False, null=False, default=0) # 文章排序 + show_toc = models.BooleanField(_('show toc'), blank=False, null=False, default=False) # 是否显示目录 category = models.ForeignKey( 'Category', verbose_name=_('category'), on_delete=models.CASCADE, blank=False, - null=False) - tags = models.ManyToManyField('Tag', verbose_name=_('tag'), blank=True) + null=False) # 分类外键 + tags = models.ManyToManyField('Tag', verbose_name=_('tag'), blank=True) # 标签多对多关系 def body_to_string(self): + # 返回正文字符串 return self.body def __str__(self): + # 对象字符串表示 return self.title class Meta: - ordering = ['-article_order', '-pub_time'] - verbose_name = _('article') - verbose_name_plural = verbose_name - get_latest_by = 'id' + ordering = ['-article_order', '-pub_time'] # 默认排序:按文章排序倒序,发布时间倒序 + verbose_name = _('article') # 单数显示名称 + verbose_name_plural = verbose_name # 复数显示名称 + get_latest_by = 'id' # 指定获取最新记录的字段 def get_absolute_url(self): + # 获取文章绝对URL return reverse('blog:detailbyid', kwargs={ 'article_id': self.id, 'year': self.creation_time.year, @@ -125,252 +133,269 @@ class Article(BaseModel): 'day': self.creation_time.day }) - @cache_decorator(60 * 60 * 10) + @cache_decorator(60 * 60 * 10) # 缓存10小时 def get_category_tree(self): - tree = self.category.get_category_tree() - names = list(map(lambda c: (c.name, c.get_absolute_url()), tree)) + # 获取分类树 + tree = self.category.get_category_tree() # 获取分类树 + names = list(map(lambda c: (c.name, c.get_absolute_url()), tree)) # 转换为名称和URL的元组列表 return names def save(self, *args, **kwargs): + # 保存文章 super().save(*args, **kwargs) def viewed(self): + # 增加浏览量 self.views += 1 - self.save(update_fields=['views']) + self.save(update_fields=['views']) # 只更新views字段 def comment_list(self): - cache_key = 'article_comments_{id}'.format(id=self.id) - value = cache.get(cache_key) + # 获取评论列表(带缓存) + cache_key = 'article_comments_{id}'.format(id=self.id) # 缓存键 + value = cache.get(cache_key) # 尝试从缓存获取 if value: - logger.info('get article comments:{id}'.format(id=self.id)) + logger.info('get article comments:{id}'.format(id=self.id)) # 记录缓存命中日志 return value else: - comments = self.comment_set.filter(is_enable=True).order_by('-id') - cache.set(cache_key, comments, 60 * 100) - logger.info('set article comments:{id}'.format(id=self.id)) + comments = self.comment_set.filter(is_enable=True).order_by('-id') # 获取有效评论并按ID倒序 + cache.set(cache_key, comments, 60 * 100) # 设置缓存,有效期100分钟 + logger.info('set article comments:{id}'.format(id=self.id)) # 记录缓存设置日志 return comments def get_admin_url(self): - info = (self._meta.app_label, self._meta.model_name) - return reverse('admin:%s_%s_change' % info, args=(self.pk,)) + # 获取管理后台URL + info = (self._meta.app_label, self._meta.model_name) # 获取应用和模型信息 + return reverse('admin:%s_%s_change' % info, args=(self.pk,)) # 生成管理后台编辑URL - @cache_decorator(expiration=60 * 100) + @cache_decorator(expiration=60 * 100) # 缓存100分钟 def next_article(self): - # 下一篇 + # 获取下一篇文章 return Article.objects.filter( - id__gt=self.id, status='p').order_by('id').first() + id__gt=self.id, status='p').order_by('id').first() # 获取ID大于当前文章的第一篇已发布文章 - @cache_decorator(expiration=60 * 100) + @cache_decorator(expiration=60 * 100) # 缓存100分钟 def prev_article(self): - # 前一篇 - return Article.objects.filter(id__lt=self.id, status='p').first() + # 获取上一篇文章 + return Article.objects.filter(id__lt=self.id, status='p').first() # 获取ID小于当前文章的第一篇已发布文章 def get_first_image_url(self): """ - Get the first image url from article.body. + 从文章正文中获取第一张图片URL :return: """ - match = re.search(r'!\[.*?\]\((.+?)\)', self.body) + match = re.search(r'!\[.*?\]\((.+?)\)', self.body) # 使用正则表达式匹配Markdown图片语法 if match: - return match.group(1) - return "" + return match.group(1) # 返回图片URL + return "" # 没有图片返回空字符串 class Category(BaseModel): - """文章分类""" - name = models.CharField(_('category name'), max_length=30, unique=True) + """文章分类模型""" + name = models.CharField(_('category name'), max_length=30, unique=True) # 分类名称 parent_category = models.ForeignKey( 'self', verbose_name=_('parent category'), blank=True, null=True, - on_delete=models.CASCADE) - slug = models.SlugField(default='no-slug', max_length=60, blank=True) - index = models.IntegerField(default=0, verbose_name=_('index')) + on_delete=models.CASCADE) # 父级分类,自关联 + slug = models.SlugField(default='no-slug', max_length=60, blank=True) # URL友好标识 + index = models.IntegerField(default=0, verbose_name=_('index')) # 排序索引 class Meta: - ordering = ['-index'] - verbose_name = _('category') - verbose_name_plural = verbose_name + ordering = ['-index'] # 默认按索引倒序排列 + verbose_name = _('category') # 单数显示名称 + verbose_name_plural = verbose_name # 复数显示名称 def get_absolute_url(self): + # 获取分类绝对URL return reverse( 'blog:category_detail', kwargs={ - 'category_name': self.slug}) + 'category_name': self.slug}) # 使用slug作为URL参数 def __str__(self): + # 对象字符串表示 return self.name - @cache_decorator(60 * 60 * 10) + @cache_decorator(60 * 60 * 10) # 缓存10小时 def get_category_tree(self): """ 递归获得分类目录的父级 :return: """ - categorys = [] + categorys = [] # 存储分类树 def parse(category): - categorys.append(category) - if category.parent_category: - parse(category.parent_category) + # 递归解析分类树 + categorys.append(category) # 添加当前分类 + if category.parent_category: # 如果有父级分类 + parse(category.parent_category) # 递归解析父级分类 - parse(self) + parse(self) # 从当前分类开始解析 return categorys - @cache_decorator(60 * 60 * 10) + @cache_decorator(60 * 60 * 10) # 缓存10小时 def get_sub_categorys(self): """ 获得当前分类目录所有子集 :return: """ - categorys = [] - all_categorys = Category.objects.all() + categorys = [] # 存储子分类 + all_categorys = Category.objects.all() # 获取所有分类 def parse(category): + # 递归解析子分类 if category not in categorys: - categorys.append(category) - childs = all_categorys.filter(parent_category=category) + categorys.append(category) # 添加当前分类 + childs = all_categorys.filter(parent_category=category) # 获取直接子分类 for child in childs: if category not in categorys: - categorys.append(child) - parse(child) + categorys.append(child) # 添加子分类 + parse(child) # 递归解析子分类的子分类 - parse(self) + parse(self) # 从当前分类开始解析 return categorys class Tag(BaseModel): - """文章标签""" - name = models.CharField(_('tag name'), max_length=30, unique=True) - slug = models.SlugField(default='no-slug', max_length=60, blank=True) + """文章标签模型""" + name = models.CharField(_('tag name'), max_length=30, unique=True) # 标签名称 + slug = models.SlugField(default='no-slug', max_length=60, blank=True) # URL友好标识 def __str__(self): + # 对象字符串表示 return self.name def get_absolute_url(self): - return reverse('blog:tag_detail', kwargs={'tag_name': self.slug}) + # 获取标签绝对URL + return reverse('blog:tag_detail', kwargs={'tag_name': self.slug}) # 使用slug作为URL参数 - @cache_decorator(60 * 60 * 10) + @cache_decorator(60 * 60 * 10) # 缓存10小时 def get_article_count(self): - return Article.objects.filter(tags__name=self.name).distinct().count() + # 获取标签下的文章数量 + return Article.objects.filter(tags__name=self.name).distinct().count() # 统计不重复的文章数量 class Meta: - ordering = ['name'] - verbose_name = _('tag') - verbose_name_plural = verbose_name + ordering = ['name'] # 默认按名称升序排列 + verbose_name = _('tag') # 单数显示名称 + verbose_name_plural = verbose_name # 复数显示名称 class Links(models.Model): - """友情链接""" + """友情链接模型""" - name = models.CharField(_('link name'), max_length=30, unique=True) - link = models.URLField(_('link')) - sequence = models.IntegerField(_('order'), unique=True) + name = models.CharField(_('link name'), max_length=30, unique=True) # 链接名称 + link = models.URLField(_('link')) # 链接地址 + sequence = models.IntegerField(_('order'), unique=True) # 排序序号 is_enable = models.BooleanField( - _('is show'), default=True, blank=False, null=False) + _('is show'), default=True, blank=False, null=False) # 是否显示 show_type = models.CharField( _('show type'), max_length=1, choices=LinkShowType.choices, - default=LinkShowType.I) - creation_time = models.DateTimeField(_('creation time'), default=now) - last_mod_time = models.DateTimeField(_('modify time'), default=now) + default=LinkShowType.I) # 显示类型 + creation_time = models.DateTimeField(_('creation time'), default=now) # 创建时间 + last_mod_time = models.DateTimeField(_('modify time'), default=now) # 最后修改时间 class Meta: - ordering = ['sequence'] - verbose_name = _('link') - verbose_name_plural = verbose_name + ordering = ['sequence'] # 默认按序号升序排列 + verbose_name = _('link') # 单数显示名称 + verbose_name_plural = verbose_name # 复数显示名称 def __str__(self): + # 对象字符串表示 return self.name class SideBar(models.Model): - """侧边栏,可以展示一些html内容""" - name = models.CharField(_('title'), max_length=100) - content = models.TextField(_('content')) - sequence = models.IntegerField(_('order'), unique=True) - is_enable = models.BooleanField(_('is enable'), default=True) - creation_time = models.DateTimeField(_('creation time'), default=now) - last_mod_time = models.DateTimeField(_('modify time'), default=now) + """侧边栏模型,可以展示一些html内容""" + name = models.CharField(_('title'), max_length=100) # 侧边栏标题 + content = models.TextField(_('content')) # 侧边栏内容 + sequence = models.IntegerField(_('order'), unique=True) # 排序序号 + is_enable = models.BooleanField(_('is enable'), default=True) # 是否启用 + creation_time = models.DateTimeField(_('creation time'), default=now) # 创建时间 + last_mod_time = models.DateTimeField(_('modify time'), default=now) # 最后修改时间 class Meta: - ordering = ['sequence'] - verbose_name = _('sidebar') - verbose_name_plural = verbose_name + ordering = ['sequence'] # 默认按序号升序排列 + verbose_name = _('sidebar') # 单数显示名称 + verbose_name_plural = verbose_name # 复数显示名称 def __str__(self): + # 对象字符串表示 return self.name class BlogSettings(models.Model): - """blog的配置""" + """博客配置模型""" site_name = models.CharField( _('site name'), max_length=200, null=False, blank=False, - default='') + default='') # 网站名称 site_description = models.TextField( _('site description'), max_length=1000, null=False, blank=False, - default='') + default='') # 网站描述 site_seo_description = models.TextField( - _('site seo description'), max_length=1000, null=False, blank=False, default='') + _('site seo description'), max_length=1000, null=False, blank=False, default='') # 网站SEO描述 site_keywords = models.TextField( _('site keywords'), max_length=1000, null=False, blank=False, - default='') - article_sub_length = models.IntegerField(_('article sub length'), default=300) - sidebar_article_count = models.IntegerField(_('sidebar article count'), default=10) - sidebar_comment_count = models.IntegerField(_('sidebar comment count'), default=5) - article_comment_count = models.IntegerField(_('article comment count'), default=5) - show_google_adsense = models.BooleanField(_('show adsense'), default=False) + default='') # 网站关键词 + article_sub_length = models.IntegerField(_('article sub length'), default=300) # 文章摘要长度 + sidebar_article_count = models.IntegerField(_('sidebar article count'), default=10) # 侧边栏文章数量 + sidebar_comment_count = models.IntegerField(_('sidebar comment count'), default=5) # 侧边栏评论数量 + article_comment_count = models.IntegerField(_('article comment count'), default=5) # 文章页面评论数量 + show_google_adsense = models.BooleanField(_('show adsense'), default=False) # 是否显示Google广告 google_adsense_codes = models.TextField( - _('adsense code'), max_length=2000, null=True, blank=True, default='') - open_site_comment = models.BooleanField(_('open site comment'), default=True) - global_header = models.TextField("公共头部", null=True, blank=True, default='') - global_footer = models.TextField("公共尾部", null=True, blank=True, default='') + _('adsense code'), max_length=2000, null=True, blank=True, default='') # Google广告代码 + open_site_comment = models.BooleanField(_('open site comment'), default=True) # 是否开启网站评论 + global_header = models.TextField("公共头部", null=True, blank=True, default='') # 全局头部内容 + global_footer = models.TextField("公共尾部", null=True, blank=True, default='') # 全局尾部内容 beian_code = models.CharField( '备案号', max_length=2000, null=True, blank=True, - default='') + default='') # 备案号 analytics_code = models.TextField( "网站统计代码", max_length=1000, null=False, blank=False, - default='') + default='') # 网站统计代码 show_gongan_code = models.BooleanField( - '是否显示公安备案号', default=False, null=False) + '是否显示公安备案号', default=False, null=False) # 是否显示公安备案号 gongan_beiancode = models.TextField( '公安备案号', max_length=2000, null=True, blank=True, - default='') + default='') # 公安备案号 comment_need_review = models.BooleanField( - '评论是否需要审核', default=False, null=False) + '评论是否需要审核', default=False, null=False) # 评论是否需要审核 class Meta: - verbose_name = _('Website configuration') - verbose_name_plural = verbose_name + verbose_name = _('Website configuration') # 单数显示名称 + verbose_name_plural = verbose_name # 复数显示名称 def __str__(self): + # 对象字符串表示 return self.site_name def clean(self): + # 数据清理验证,确保只能有一个配置实例 if BlogSettings.objects.exclude(id=self.id).count(): - raise ValidationError(_('There can only be one configuration')) + raise ValidationError(_('There can only be one configuration')) # 只能有一个配置 def save(self, *args, **kwargs): + # 保存配置,同时清除缓存 super().save(*args, **kwargs) from djangoblog.utils import cache - cache.clear() + cache.clear() # 清除所有缓存 \ No newline at end of file diff --git a/src/DjangoBlog/blog/search_indexes.py b/src/DjangoBlog/blog/search_indexes.py index 7f1dfac..0d793ca 100644 --- a/src/DjangoBlog/blog/search_indexes.py +++ b/src/DjangoBlog/blog/search_indexes.py @@ -4,10 +4,13 @@ from blog.models import Article class ArticleIndex(indexes.SearchIndex, indexes.Indexable): - text = indexes.CharField(document=True, use_template=True) + # Haystack搜索索引类,用于全文搜索 + text = indexes.CharField(document=True, use_template=True) # 主搜索字段,使用模板定义内容 def get_model(self): + # 返回与此索引关联的模型类 return Article def index_queryset(self, using=None): - return self.get_model().objects.filter(status='p') + # 返回要建立索引的查询集,只包含已发布的文章 + return self.get_model().objects.filter(status='p') \ No newline at end of file diff --git a/src/DjangoBlog/blog/templatetags/blog_tags.py b/src/DjangoBlog/blog/templatetags/blog_tags.py index 1f994bc..a4d376b 100644 --- a/src/DjangoBlog/blog/templatetags/blog_tags.py +++ b/src/DjangoBlog/blog/templatetags/blog_tags.py @@ -22,18 +22,20 @@ from djangoblog.plugin_manage import hooks logger = logging.getLogger(__name__) -register = template.Library() +register = template.Library() # 创建模板标签库实例 @register.simple_tag(takes_context=True) def head_meta(context): + # 头部meta标签,应用插件过滤器 return mark_safe(hooks.apply_filters('head_meta', '', context)) @register.simple_tag def timeformat(data): + # 时间格式化标签 try: - return data.strftime(settings.TIME_FORMAT) + return data.strftime(settings.TIME_FORMAT) # 使用设置中的时间格式 except Exception as e: logger.error(e) return "" @@ -41,8 +43,9 @@ def timeformat(data): @register.simple_tag def datetimeformat(data): + # 日期时间格式化标签 try: - return data.strftime(settings.DATE_TIME_FORMAT) + return data.strftime(settings.DATE_TIME_FORMAT) # 使用设置中的日期时间格式 except Exception as e: logger.error(e) return "" @@ -55,19 +58,20 @@ def custom_markdown(content): 通用markdown过滤器,应用文章内容插件 主要用于文章内容处理 """ - html_content = CommonMarkdown.get_markdown(content) - + html_content = CommonMarkdown.get_markdown(content) # 将markdown转换为HTML + # 然后应用插件过滤器优化HTML from djangoblog.plugin_manage import hooks from djangoblog.plugin_manage.hook_constants import ARTICLE_CONTENT_HOOK_NAME - optimized_html = hooks.apply_filters(ARTICLE_CONTENT_HOOK_NAME, html_content) - - return mark_safe(optimized_html) + optimized_html = hooks.apply_filters(ARTICLE_CONTENT_HOOK_NAME, html_content) # 应用文章内容插件 + + return mark_safe(optimized_html) # 标记为安全HTML @register.filter() @stringfilter def sidebar_markdown(content): + # 侧边栏markdown过滤器 html_content = CommonMarkdown.get_markdown(content) return mark_safe(html_content) @@ -76,7 +80,7 @@ def sidebar_markdown(content): def render_article_content(context, article, is_summary=False): """ 渲染文章内容,包含完整的上下文信息供插件使用 - + Args: context: 模板上下文 article: 文章对象 @@ -84,56 +88,58 @@ def render_article_content(context, article, is_summary=False): """ if not article or not hasattr(article, 'body'): return '' - + # 先转换Markdown为HTML html_content = CommonMarkdown.get_markdown(article.body) - + # 如果是摘要模式,先截断内容再应用插件 if is_summary: # 截断HTML内容到合适的长度(约300字符) from django.utils.html import strip_tags from django.template.defaultfilters import truncatechars - + # 先去除HTML标签,截断纯文本,然后重新转换为HTML plain_text = strip_tags(html_content) truncated_text = truncatechars(plain_text, 300) - + # 重新转换截断后的文本为HTML(简化版,避免复杂的插件处理) html_content = CommonMarkdown.get_markdown(truncated_text) - + # 然后应用插件过滤器,传递完整的上下文 from djangoblog.plugin_manage import hooks from djangoblog.plugin_manage.hook_constants import ARTICLE_CONTENT_HOOK_NAME - + # 获取request对象 request = context.get('request') - + # 应用所有文章内容相关的插件 # 注意:摘要模式下某些插件(如版权声明)可能不适用 optimized_html = hooks.apply_filters( - ARTICLE_CONTENT_HOOK_NAME, - html_content, - article=article, + ARTICLE_CONTENT_HOOK_NAME, + html_content, + article=article, request=request, context=context, is_summary=is_summary # 传递摘要标志,插件可以据此调整行为 ) - + return mark_safe(optimized_html) @register.simple_tag def get_markdown_toc(content): + # 获取markdown目录 from djangoblog.utils import CommonMarkdown - body, toc = CommonMarkdown.get_markdown_with_toc(content) + body, toc = CommonMarkdown.get_markdown_with_toc(content) # 获取带目录的markdown return mark_safe(toc) @register.filter() @stringfilter def comment_markdown(content): + # 评论markdown过滤器,并进行HTML清理 content = CommonMarkdown.get_markdown(content) - return mark_safe(sanitize_html(content)) + return mark_safe(sanitize_html(content)) # 清理不安全的HTML @register.filter(is_safe=True) @@ -146,13 +152,14 @@ def truncatechars_content(content): """ from django.template.defaultfilters import truncatechars_html from djangoblog.utils import get_blog_setting - blogsetting = get_blog_setting() - return truncatechars_html(content, blogsetting.article_sub_length) + blogsetting = get_blog_setting() # 获取博客设置 + return truncatechars_html(content, blogsetting.article_sub_length) # 根据设置截断HTML内容 @register.filter(is_safe=True) @stringfilter def truncate(content): + # 简单截断过滤器,去除HTML标签后截取前150字符 from django.utils.html import strip_tags return strip_tags(content)[:150] @@ -165,12 +172,12 @@ def load_breadcrumb(article): :param article: :return: """ - names = article.get_category_tree() + names = article.get_category_tree() # 获取分类树 from djangoblog.utils import get_blog_setting blogsetting = get_blog_setting() site = get_current_site().domain - names.append((blogsetting.site_name, '/')) - names = names[::-1] + names.append((blogsetting.site_name, '/')) # 添加网站首页 + names = names[::-1] # 反转列表顺序 return { 'names': names, @@ -189,10 +196,10 @@ def load_articletags(article): tags = article.tags.all() tags_list = [] for tag in tags: - url = tag.get_absolute_url() - count = tag.get_article_count() + url = tag.get_absolute_url() # 获取标签URL + count = tag.get_article_count() # 获取标签下文章数量 tags_list.append(( - url, count, tag, random.choice(settings.BOOTSTRAP_COLOR_TYPES) + url, count, tag, random.choice(settings.BOOTSTRAP_COLOR_TYPES) # 随机选择Bootstrap颜色 )) return { 'article_tags_list': tags_list @@ -205,7 +212,7 @@ def load_sidebar(user, linktype): 加载侧边栏 :return: """ - value = cache.get("sidebar" + linktype) + value = cache.get("sidebar" + linktype) # 尝试从缓存获取侧边栏 if value: value['user'] = user return value @@ -214,30 +221,30 @@ def load_sidebar(user, linktype): from djangoblog.utils import get_blog_setting blogsetting = get_blog_setting() recent_articles = Article.objects.filter( - status='p')[:blogsetting.sidebar_article_count] - sidebar_categorys = Category.objects.all() + status='p')[:blogsetting.sidebar_article_count] # 最近文章 + sidebar_categorys = Category.objects.all() # 全部分类 extra_sidebars = SideBar.objects.filter( - is_enable=True).order_by('sequence') + is_enable=True).order_by('sequence') # 额外侧边栏 most_read_articles = Article.objects.filter(status='p').order_by( - '-views')[:blogsetting.sidebar_article_count] - dates = Article.objects.datetimes('creation_time', 'month', order='DESC') + '-views')[:blogsetting.sidebar_article_count] # 最多阅读文章 + dates = Article.objects.datetimes('creation_time', 'month', order='DESC') # 文章归档日期 links = Links.objects.filter(is_enable=True).filter( - Q(show_type=str(linktype)) | Q(show_type=LinkShowType.A)) + Q(show_type=str(linktype)) | Q(show_type=LinkShowType.A)) # 友情链接 commment_list = Comment.objects.filter(is_enable=True).order_by( - '-id')[:blogsetting.sidebar_comment_count] + '-id')[:blogsetting.sidebar_comment_count] # 最新评论 # 标签云 计算字体大小 # 根据总数计算出平均值 大小为 (数目/平均值)*步长 increment = 5 tags = Tag.objects.all() sidebar_tags = None if tags and len(tags) > 0: - s = [t for t in [(t, t.get_article_count()) for t in tags] if t[1]] - count = sum([t[1] for t in s]) - dd = 1 if (count == 0 or not len(tags)) else count / len(tags) + s = [t for t in [(t, t.get_article_count()) for t in tags] if t[1]] # 过滤有文章的标签 + count = sum([t[1] for t in s]) # 计算总文章数 + dd = 1 if (count == 0 or not len(tags)) else count / len(tags) # 计算平均值 import random sidebar_tags = list( - map(lambda x: (x[0], x[1], (x[1] / dd) * increment + 10), s)) - random.shuffle(sidebar_tags) + map(lambda x: (x[0], x[1], (x[1] / dd) * increment + 10), s)) # 计算标签字体大小 + random.shuffle(sidebar_tags) # 随机打乱标签顺序 value = { 'recent_articles': recent_articles, @@ -253,7 +260,7 @@ def load_sidebar(user, linktype): 'sidebar_tags': sidebar_tags, 'extra_sidebars': extra_sidebars } - cache.set("sidebar" + linktype, value, 60 * 60 * 60 * 3) + cache.set("sidebar" + linktype, value, 60 * 60 * 60 * 3) # 缓存3小时 logger.info('set sidebar cache.key:{key}'.format(key="sidebar" + linktype)) value['user'] = user return value @@ -274,9 +281,10 @@ def load_article_metas(article, user): @register.inclusion_tag('blog/tags/article_pagination.html') def load_pagination_info(page_obj, page_type, tag_name): + # 分页信息标签 previous_url = '' next_url = '' - if page_type == '': + if page_type == '': # 首页分页 if page_obj.has_next(): next_number = page_obj.next_page_number() next_url = reverse('blog:index_page', kwargs={'page': next_number}) @@ -285,7 +293,7 @@ def load_pagination_info(page_obj, page_type, tag_name): previous_url = reverse( 'blog:index_page', kwargs={ 'page': previous_number}) - if page_type == '分类标签归档': + if page_type == '分类标签归档': # 标签分页 tag = get_object_or_404(Tag, name=tag_name) if page_obj.has_next(): next_number = page_obj.next_page_number() @@ -301,7 +309,7 @@ def load_pagination_info(page_obj, page_type, tag_name): kwargs={ 'page': previous_number, 'tag_name': tag.slug}) - if page_type == '作者文章归档': + if page_type == '作者文章归档': # 作者分页 if page_obj.has_next(): next_number = page_obj.next_page_number() next_url = reverse( @@ -317,7 +325,7 @@ def load_pagination_info(page_obj, page_type, tag_name): 'page': previous_number, 'author_name': tag_name}) - if page_type == '分类目录归档': + if page_type == '分类目录归档': # 分类分页 category = get_object_or_404(Category, name=tag_name) if page_obj.has_next(): next_number = page_obj.next_page_number() @@ -366,10 +374,10 @@ def load_article_detail(article, isindex, user): def gravatar_url(email, size=40): """获得用户头像 - 优先使用OAuth头像,否则使用默认头像""" cachekey = 'avatar/' + email - url = cache.get(cachekey) + url = cache.get(cachekey) # 尝试从缓存获取头像 if url: return url - + # 检查OAuth用户是否有自定义头像 usermodels = OAuthUser.objects.filter(email=email) if usermodels: @@ -378,18 +386,19 @@ def gravatar_url(email, size=40): if users_with_picture: # 获取默认头像路径用于比较 default_avatar_path = static('blog/img/avatar.png') - + # 优先选择非默认头像的用户,否则选择第一个 - non_default_users = [u for u in users_with_picture if u.picture != default_avatar_path and not u.picture.endswith('/avatar.png')] + non_default_users = [u for u in users_with_picture if + u.picture != default_avatar_path and not u.picture.endswith('/avatar.png')] selected_user = non_default_users[0] if non_default_users else users_with_picture[0] - + url = selected_user.picture cache.set(cachekey, url, 60 * 60 * 24) # 缓存24小时 - + avatar_type = 'non-default' if non_default_users else 'default' logger.info('Using {} OAuth avatar for {} from {}'.format(avatar_type, email, selected_user.type)) return url - + # 使用默认头像 url = static('blog/img/avatar.png') cache.set(cachekey, url, 60 * 60 * 24) # 缓存24小时 @@ -403,7 +412,7 @@ def gravatar(email, size=40): url = gravatar_url(email, size) return mark_safe( '用户头像' % - (url, size, size)) + (url, size, size)) # 生成头像img标签 @register.simple_tag @@ -414,10 +423,10 @@ def query(qs, **kwargs): ... {% endfor %} """ - return qs.filter(**kwargs) + return qs.filter(**kwargs) # 查询集过滤 @register.filter def addstr(arg1, arg2): """concatenate arg1 & arg2""" - return str(arg1) + str(arg2) + return str(arg1) + str(arg2) # 字符串连接 \ No newline at end of file diff --git a/src/DjangoBlog/blog/tests.py b/src/DjangoBlog/blog/tests.py index ee13505..dd235d3 100644 --- a/src/DjangoBlog/blog/tests.py +++ b/src/DjangoBlog/blog/tests.py @@ -21,54 +21,57 @@ from oauth.models import OAuthUser, OAuthConfig class ArticleTest(TestCase): def setUp(self): - self.client = Client() - self.factory = RequestFactory() + # 测试初始化设置 + self.client = Client() # 创建测试客户端 + self.factory = RequestFactory() # 创建请求工厂 def test_validate_article(self): + # 测试文章验证功能 site = get_current_site().domain user = BlogUser.objects.get_or_create( email="liangliangyy@gmail.com", - username="liangliangyy")[0] - user.set_password("liangliangyy") - user.is_staff = True - user.is_superuser = True + username="liangliangyy")[0] # 获取或创建测试用户 + user.set_password("liangliangyy") # 设置密码 + user.is_staff = True # 设置为员工 + user.is_superuser = True # 设置为超级用户 user.save() - response = self.client.get(user.get_absolute_url()) - self.assertEqual(response.status_code, 200) - response = self.client.get('/admin/servermanager/emailsendlog/') - response = self.client.get('admin/admin/logentry/') - s = SideBar() - s.sequence = 1 - s.name = 'test' - s.content = 'test content' - s.is_enable = True + response = self.client.get(user.get_absolute_url()) # 访问用户详情页 + self.assertEqual(response.status_code, 200) # 验证访问成功 + response = self.client.get('/admin/servermanager/emailsendlog/') # 访问邮件发送日志管理页 + response = self.client.get('admin/admin/logentry/') # 访问日志条目管理页 + s = SideBar() # 创建侧边栏 + s.sequence = 1 # 设置排序 + s.name = 'test' # 设置名称 + s.content = 'test content' # 设置内容 + s.is_enable = True # 启用侧边栏 s.save() - category = Category() - category.name = "category" - category.creation_time = timezone.now() - category.last_mod_time = timezone.now() + category = Category() # 创建分类 + category.name = "category" # 设置分类名称 + category.creation_time = timezone.now() # 设置创建时间 + category.last_mod_time = timezone.now() # 设置修改时间 category.save() - tag = Tag() - tag.name = "nicetag" + tag = Tag() # 创建标签 + tag.name = "nicetag" # 设置标签名称 tag.save() - article = Article() - article.title = "nicetitle" - article.body = "nicecontent" - article.author = user - article.category = category - article.type = 'a' - article.status = 'p' + article = Article() # 创建文章 + article.title = "nicetitle" # 设置文章标题 + article.body = "nicecontent" # 设置文章内容 + article.author = user # 设置作者 + article.category = category # 设置分类 + article.type = 'a' # 设置类型为文章 + article.status = 'p' # 设置状态为已发布 article.save() - self.assertEqual(0, article.tags.count()) - article.tags.add(tag) + self.assertEqual(0, article.tags.count()) # 验证初始标签数为0 + article.tags.add(tag) # 添加标签 article.save() - self.assertEqual(1, article.tags.count()) + self.assertEqual(1, article.tags.count()) # 验证标签数变为1 for i in range(20): + # 批量创建20篇文章 article = Article() article.title = "nicetitle" + str(i) article.body = "nicetitle" + str(i) @@ -81,152 +84,156 @@ class ArticleTest(TestCase): article.save() from blog.documents import ELASTICSEARCH_ENABLED if ELASTICSEARCH_ENABLED: - call_command("build_index") - response = self.client.get('/search', {'q': 'nicetitle'}) - self.assertEqual(response.status_code, 200) + call_command("build_index") # 构建搜索索引 + response = self.client.get('/search', {'q': 'nicetitle'}) # 搜索文章 + self.assertEqual(response.status_code, 200) # 验证搜索成功 - response = self.client.get(article.get_absolute_url()) - self.assertEqual(response.status_code, 200) + response = self.client.get(article.get_absolute_url()) # 访问文章详情页 + self.assertEqual(response.status_code, 200) # 验证访问成功 from djangoblog.spider_notify import SpiderNotify - SpiderNotify.notify(article.get_absolute_url()) - response = self.client.get(tag.get_absolute_url()) - self.assertEqual(response.status_code, 200) + SpiderNotify.notify(article.get_absolute_url()) # 通知搜索引擎 + response = self.client.get(tag.get_absolute_url()) # 访问标签详情页 + self.assertEqual(response.status_code, 200) # 验证访问成功 - response = self.client.get(category.get_absolute_url()) - self.assertEqual(response.status_code, 200) + response = self.client.get(category.get_absolute_url()) # 访问分类详情页 + self.assertEqual(response.status_code, 200) # 验证访问成功 - response = self.client.get('/search', {'q': 'django'}) - self.assertEqual(response.status_code, 200) - s = load_articletags(article) - self.assertIsNotNone(s) + response = self.client.get('/search', {'q': 'django'}) # 搜索django相关内容 + self.assertEqual(response.status_code, 200) # 验证搜索成功 + s = load_articletags(article) # 加载文章标签 + self.assertIsNotNone(s) # 验证标签加载成功 - self.client.login(username='liangliangyy', password='liangliangyy') + self.client.login(username='liangliangyy', password='liangliangyy') # 登录用户 - response = self.client.get(reverse('blog:archives')) - self.assertEqual(response.status_code, 200) + response = self.client.get(reverse('blog:archives')) # 访问文章归档页 + self.assertEqual(response.status_code, 200) # 验证访问成功 - p = Paginator(Article.objects.all(), settings.PAGINATE_BY) - self.check_pagination(p, '', '') + p = Paginator(Article.objects.all(), settings.PAGINATE_BY) # 创建所有文章的分页器 + self.check_pagination(p, '', '') # 测试分页功能 - p = Paginator(Article.objects.filter(tags=tag), settings.PAGINATE_BY) - self.check_pagination(p, '分类标签归档', tag.slug) + p = Paginator(Article.objects.filter(tags=tag), settings.PAGINATE_BY) # 创建标签文章的分页器 + self.check_pagination(p, '分类标签归档', tag.slug) # 测试标签分页 p = Paginator( Article.objects.filter( - author__username='liangliangyy'), settings.PAGINATE_BY) - self.check_pagination(p, '作者文章归档', 'liangliangyy') + author__username='liangliangyy'), settings.PAGINATE_BY) # 创建作者文章的分页器 + self.check_pagination(p, '作者文章归档', 'liangliangyy') # 测试作者分页 - p = Paginator(Article.objects.filter(category=category), settings.PAGINATE_BY) - self.check_pagination(p, '分类目录归档', category.slug) + p = Paginator(Article.objects.filter(category=category), settings.PAGINATE_BY) # 创建分类文章的分页器 + self.check_pagination(p, '分类目录归档', category.slug) # 测试分类分页 - f = BlogSearchForm() - f.search() + f = BlogSearchForm() # 创建搜索表单 + f.search() # 执行搜索 # self.client.login(username='liangliangyy', password='liangliangyy') from djangoblog.spider_notify import SpiderNotify - SpiderNotify.baidu_notify([article.get_full_url()]) + SpiderNotify.baidu_notify([article.get_full_url()]) # 通知百度搜索引擎 from blog.templatetags.blog_tags import gravatar_url, gravatar - u = gravatar_url('liangliangyy@gmail.com') - u = gravatar('liangliangyy@gmail.com') + u = gravatar_url('liangliangyy@gmail.com') # 获取头像URL + u = gravatar('liangliangyy@gmail.com') # 获取头像HTML - link = Links( + link = Links( # 创建友情链接 sequence=1, name="lylinux", link='https://wwww.lylinux.net') link.save() - response = self.client.get('/links.html') - self.assertEqual(response.status_code, 200) + response = self.client.get('/links.html') # 访问友情链接页 + self.assertEqual(response.status_code, 200) # 验证访问成功 - response = self.client.get('/feed/') - self.assertEqual(response.status_code, 200) + response = self.client.get('/feed/') # 访问RSS订阅 + self.assertEqual(response.status_code, 200) # 验证访问成功 - response = self.client.get('/sitemap.xml') - self.assertEqual(response.status_code, 200) + response = self.client.get('/sitemap.xml') # 访问网站地图 + self.assertEqual(response.status_code, 200) # 验证访问成功 - self.client.get("/admin/blog/article/1/delete/") - self.client.get('/admin/servermanager/emailsendlog/') - self.client.get('/admin/admin/logentry/') - self.client.get('/admin/admin/logentry/1/change/') + self.client.get("/admin/blog/article/1/delete/") # 访问文章删除页 + self.client.get('/admin/servermanager/emailsendlog/') # 访问邮件发送日志管理页 + self.client.get('/admin/admin/logentry/') # 访问日志条目管理页 + self.client.get('/admin/admin/logentry/1/change/') # 访问日志条目编辑页 def check_pagination(self, p, type, value): - for page in range(1, p.num_pages + 1): - s = load_pagination_info(p.page(page), type, value) - self.assertIsNotNone(s) - if s['previous_url']: - response = self.client.get(s['previous_url']) - self.assertEqual(response.status_code, 200) - if s['next_url']: - response = self.client.get(s['next_url']) - self.assertEqual(response.status_code, 200) + # 检查分页功能 + for page in range(1, p.num_pages + 1): # 遍历所有分页 + s = load_pagination_info(p.page(page), type, value) # 加载分页信息 + self.assertIsNotNone(s) # 验证分页信息不为空 + if s['previous_url']: # 如果有上一页链接 + response = self.client.get(s['previous_url']) # 访问上一页 + self.assertEqual(response.status_code, 200) # 验证访问成功 + if s['next_url']: # 如果有下一页链接 + response = self.client.get(s['next_url']) # 访问下一页 + self.assertEqual(response.status_code, 200) # 验证访问成功 def test_image(self): + # 测试图片上传功能 import requests rsp = requests.get( - 'https://www.python.org/static/img/python-logo.png') - imagepath = os.path.join(settings.BASE_DIR, 'python.png') + 'https://www.python.org/static/img/python-logo.png') # 下载测试图片 + imagepath = os.path.join(settings.BASE_DIR, 'python.png') # 图片保存路径 with open(imagepath, 'wb') as file: - file.write(rsp.content) - rsp = self.client.post('/upload') - self.assertEqual(rsp.status_code, 403) - sign = get_sha256(get_sha256(settings.SECRET_KEY)) + file.write(rsp.content) # 保存图片 + rsp = self.client.post('/upload') # 尝试上传图片(无签名) + self.assertEqual(rsp.status_code, 403) # 验证被拒绝(需要签名) + sign = get_sha256(get_sha256(settings.SECRET_KEY)) # 生成上传签名 with open(imagepath, 'rb') as file: imgfile = SimpleUploadedFile( - 'python.png', file.read(), content_type='image/jpg') + 'python.png', file.read(), content_type='image/jpg') # 创建上传文件对象 form_data = {'python.png': imgfile} rsp = self.client.post( - '/upload?sign=' + sign, form_data, follow=True) - self.assertEqual(rsp.status_code, 200) - os.remove(imagepath) + '/upload?sign=' + sign, form_data, follow=True) # 带签名上传图片 + self.assertEqual(rsp.status_code, 200) # 验证上传成功 + os.remove(imagepath) # 删除临时图片文件 from djangoblog.utils import save_user_avatar, send_email - send_email(['qq@qq.com'], 'testTitle', 'testContent') + send_email(['qq@qq.com'], 'testTitle', 'testContent') # 测试发送邮件 save_user_avatar( - 'https://www.python.org/static/img/python-logo.png') + 'https://www.python.org/static/img/python-logo.png') # 测试保存用户头像 def test_errorpage(self): - rsp = self.client.get('/eee') - self.assertEqual(rsp.status_code, 404) + # 测试错误页面 + rsp = self.client.get('/eee') # 访问不存在的页面 + self.assertEqual(rsp.status_code, 404) # 验证返回404错误 def test_commands(self): + # 测试管理命令 user = BlogUser.objects.get_or_create( email="liangliangyy@gmail.com", - username="liangliangyy")[0] - user.set_password("liangliangyy") - user.is_staff = True - user.is_superuser = True + username="liangliangyy")[0] # 获取或创建测试用户 + user.set_password("liangliangyy") # 设置密码 + user.is_staff = True # 设置为员工 + user.is_superuser = True # 设置为超级用户 user.save() - c = OAuthConfig() - c.type = 'qq' - c.appkey = 'appkey' - c.appsecret = 'appsecret' + c = OAuthConfig() # 创建OAuth配置 + c.type = 'qq' # 设置类型为QQ + c.appkey = 'appkey' # 设置应用密钥 + c.appsecret = 'appsecret' # 设置应用密钥 c.save() - u = OAuthUser() - u.type = 'qq' - u.openid = 'openid' - u.user = user - u.picture = static("/blog/img/avatar.png") + u = OAuthUser() # 创建OAuth用户 + u.type = 'qq' # 设置类型为QQ + u.openid = 'openid' # 设置OpenID + u.user = user # 关联用户 + u.picture = static("/blog/img/avatar.png") # 设置头像 u.metadata = ''' { "figureurl": "https://qzapp.qlogo.cn/qzapp/101513904/C740E30B4113EAA80E0D9918ABC78E82/30" -}''' +}''' # 设置元数据 u.save() - u = OAuthUser() - u.type = 'qq' - u.openid = 'openid1' - u.picture = 'https://qzapp.qlogo.cn/qzapp/101513904/C740E30B4113EAA80E0D9918ABC78E82/30' + u = OAuthUser() # 创建另一个OAuth用户 + u.type = 'qq' # 设置类型为QQ + u.openid = 'openid1' # 设置不同的OpenID + u.picture = 'https://qzapp.qlogo.cn/qzapp/101513904/C740E30B4113EAA80E0D9918ABC78E82/30' # 设置网络头像 u.metadata = ''' { "figureurl": "https://qzapp.qlogo.cn/qzapp/101513904/C740E30B4113EAA80E0D9918ABC78E82/30" - }''' + }''' # 设置元数据 u.save() from blog.documents import ELASTICSEARCH_ENABLED if ELASTICSEARCH_ENABLED: - call_command("build_index") - call_command("ping_baidu", "all") - call_command("create_testdata") - call_command("clear_cache") - call_command("sync_user_avatar") - call_command("build_search_words") + call_command("build_index") # 构建搜索索引 + call_command("ping_baidu", "all") # 通知百度搜索引擎 + call_command("create_testdata") # 创建测试数据 + call_command("clear_cache") # 清除缓存 + call_command("sync_user_avatar") # 同步用户头像 + call_command("build_search_words") # 构建搜索词 \ No newline at end of file diff --git a/src/DjangoBlog/blog/urls.py b/src/DjangoBlog/blog/urls.py index adf2703..84737b0 100644 --- a/src/DjangoBlog/blog/urls.py +++ b/src/DjangoBlog/blog/urls.py @@ -3,60 +3,74 @@ from django.views.decorators.cache import cache_page from . import views -app_name = "blog" +app_name = "blog" # 定义应用的命名空间 + urlpatterns = [ + # 首页URL path( r'', views.IndexView.as_view(), name='index'), + # 首页分页URL path( r'page//', views.IndexView.as_view(), name='index_page'), + # 文章详情页URL(按ID和时间) path( r'article////.html', views.ArticleDetailView.as_view(), name='detailbyid'), + # 分类详情页URL path( r'category/.html', views.CategoryDetailView.as_view(), name='category_detail'), + # 分类详情分页URL path( r'category//.html', views.CategoryDetailView.as_view(), name='category_detail_page'), + # 作者详情页URL path( r'author/.html', views.AuthorDetailView.as_view(), name='author_detail'), + # 作者详情分页URL path( r'author//.html', views.AuthorDetailView.as_view(), name='author_detail_page'), + # 标签详情页URL path( r'tag/.html', views.TagDetailView.as_view(), name='tag_detail'), + # 标签详情分页URL path( r'tag//.html', views.TagDetailView.as_view(), name='tag_detail_page'), + # 文章归档页URL(缓存1小时) path( 'archives.html', cache_page( 60 * 60)( views.ArchivesView.as_view()), name='archives'), + # 友情链接页URL path( 'links.html', views.LinkListView.as_view(), name='links'), + # 文件上传URL path( r'upload', views.fileupload, name='upload'), + # 缓存清理URL path( r'clean', views.clean_cache_view, name='clean'), -] +] \ No newline at end of file diff --git a/src/DjangoBlog/blog/views.py b/src/DjangoBlog/blog/views.py index ace9e63..6c92616 100644 --- a/src/DjangoBlog/blog/views.py +++ b/src/DjangoBlog/blog/views.py @@ -25,39 +25,39 @@ logger = logging.getLogger(__name__) class ArticleListView(ListView): - # template_name属性用于指定使用哪个模板进行渲染 - template_name = 'blog/article_index.html' + # 文章列表视图基类 + template_name = 'blog/article_index.html' # 指定使用的模板 - # context_object_name属性用于给上下文变量取名(在模板中使用该名字) - context_object_name = 'article_list' + context_object_name = 'article_list' # 上下文变量名称 # 页面类型,分类目录或标签列表等 page_type = '' - paginate_by = settings.PAGINATE_BY - page_kwarg = 'page' - link_type = LinkShowType.L + paginate_by = settings.PAGINATE_BY # 分页大小 + page_kwarg = 'page' # 页码参数名 + link_type = LinkShowType.L # 链接显示类型 def get_view_cache_key(self): return self.request.get['pages'] @property def page_number(self): + # 获取当前页码 page_kwarg = self.page_kwarg page = self.kwargs.get( - page_kwarg) or self.request.GET.get(page_kwarg) or 1 + page_kwarg) or self.request.GET.get(page_kwarg) or 1 # 从URL参数或GET参数获取页码 return page def get_queryset_cache_key(self): """ 子类重写.获得queryset的缓存key """ - raise NotImplementedError() + raise NotImplementedError() # 子类必须实现此方法 def get_queryset_data(self): """ 子类重写.获取queryset的数据 """ - raise NotImplementedError() + raise NotImplementedError() # 子类必须实现此方法 def get_queryset_from_cache(self, cache_key): ''' @@ -65,14 +65,14 @@ class ArticleListView(ListView): :param cache_key: 缓存key :return: ''' - value = cache.get(cache_key) + value = cache.get(cache_key) # 尝试从缓存获取数据 if value: - logger.info('get view cache.key:{key}'.format(key=cache_key)) + logger.info('get view cache.key:{key}'.format(key=cache_key)) # 记录缓存命中日志 return value else: - article_list = self.get_queryset_data() - cache.set(cache_key, article_list) - logger.info('set view cache.key:{key}'.format(key=cache_key)) + article_list = self.get_queryset_data() # 获取数据 + cache.set(cache_key, article_list) # 设置缓存 + logger.info('set view cache.key:{key}'.format(key=cache_key)) # 记录缓存设置日志 return article_list def get_queryset(self): @@ -80,296 +80,315 @@ class ArticleListView(ListView): 重写默认,从缓存获取数据 :return: ''' - key = self.get_queryset_cache_key() - value = self.get_queryset_from_cache(key) + key = self.get_queryset_cache_key() # 获取缓存键 + value = self.get_queryset_from_cache(key) # 从缓存获取数据 return value def get_context_data(self, **kwargs): - kwargs['linktype'] = self.link_type - return super(ArticleListView, self).get_context_data(**kwargs) + kwargs['linktype'] = self.link_type # 添加链接类型到上下文 + return super(ArticleListView, self).get_context_data(**kwargs) # 调用父类方法 class IndexView(ArticleListView): ''' - 首页 + 首页视图 ''' - # 友情链接类型 - link_type = LinkShowType.I + link_type = LinkShowType.I # 首页链接类型 def get_queryset_data(self): - article_list = Article.objects.filter(type='a', status='p') + # 获取首页文章列表数据 + article_list = Article.objects.filter(type='a', status='p') # 过滤文章类型和状态 return article_list def get_queryset_cache_key(self): - cache_key = 'index_{page}'.format(page=self.page_number) + # 生成首页缓存键 + cache_key = 'index_{page}'.format(page=self.page_number) # 包含页码的缓存键 return cache_key class ArticleDetailView(DetailView): ''' - 文章详情页面 + 文章详情页面视图 ''' - template_name = 'blog/article_detail.html' - model = Article - pk_url_kwarg = 'article_id' - context_object_name = "article" + template_name = 'blog/article_detail.html' # 文章详情模板 + model = Article # 关联的模型 + pk_url_kwarg = 'article_id' # URL中的主键参数名 + context_object_name = "article" # 上下文变量名 def get_context_data(self, **kwargs): - comment_form = CommentForm() - - article_comments = self.object.comment_list() - parent_comments = article_comments.filter(parent_comment=None) - blog_setting = get_blog_setting() - paginator = Paginator(parent_comments, blog_setting.article_comment_count) - page = self.request.GET.get('comment_page', '1') - if not page.isnumeric(): + # 获取文章详情页的上下文数据 + comment_form = CommentForm() # 评论表单 + + article_comments = self.object.comment_list() # 获取文章评论列表 + parent_comments = article_comments.filter(parent_comment=None) # 获取父级评论 + blog_setting = get_blog_setting() # 获取博客设置 + paginator = Paginator(parent_comments, blog_setting.article_comment_count) # 评论分页器 + page = self.request.GET.get('comment_page', '1') # 获取评论页码 + if not page.isnumeric(): # 验证页码是否为数字 page = 1 else: page = int(page) - if page < 1: + if page < 1: # 页码不能小于1 page = 1 - if page > paginator.num_pages: + if page > paginator.num_pages: # 页码不能大于总页数 page = paginator.num_pages - p_comments = paginator.page(page) - next_page = p_comments.next_page_number() if p_comments.has_next() else None - prev_page = p_comments.previous_page_number() if p_comments.has_previous() else None + p_comments = paginator.page(page) # 获取当前页评论 + next_page = p_comments.next_page_number() if p_comments.has_next() else None # 下一页页码 + prev_page = p_comments.previous_page_number() if p_comments.has_previous() else None # 上一页页码 - if next_page: + if next_page: # 如果有下一页 kwargs[ - 'comment_next_page_url'] = self.object.get_absolute_url() + f'?comment_page={next_page}#commentlist-container' - if prev_page: + 'comment_next_page_url'] = self.object.get_absolute_url() + f'?comment_page={next_page}#commentlist-container' # 构建下一页URL + if prev_page: # 如果有上一页 kwargs[ - 'comment_prev_page_url'] = self.object.get_absolute_url() + f'?comment_page={prev_page}#commentlist-container' - kwargs['form'] = comment_form - kwargs['article_comments'] = article_comments - kwargs['p_comments'] = p_comments + 'comment_prev_page_url'] = self.object.get_absolute_url() + f'?comment_page={prev_page}#commentlist-container' # 构建上一页URL + kwargs['form'] = comment_form # 评论表单 + kwargs['article_comments'] = article_comments # 所有评论 + kwargs['p_comments'] = p_comments # 当前页评论 kwargs['comment_count'] = len( - article_comments) if article_comments else 0 + article_comments) if article_comments else 0 # 评论总数 - kwargs['next_article'] = self.object.next_article - kwargs['prev_article'] = self.object.prev_article + kwargs['next_article'] = self.object.next_article # 下一篇文章 + kwargs['prev_article'] = self.object.prev_article # 上一篇文章 - context = super(ArticleDetailView, self).get_context_data(**kwargs) + context = super(ArticleDetailView, self).get_context_data(**kwargs) # 调用父类方法 article = self.object # Action Hook, 通知插件"文章详情已获取" - hooks.run_action('after_article_body_get', article=article, request=self.request) + hooks.run_action('after_article_body_get', article=article, request=self.request) # 执行插件动作 return context class CategoryDetailView(ArticleListView): ''' - 分类目录列表 + 分类目录列表视图 ''' - page_type = "分类目录归档" + page_type = "分类目录归档" # 页面类型 def get_queryset_data(self): - slug = self.kwargs['category_name'] - category = get_object_or_404(Category, slug=slug) + # 获取分类目录下的文章数据 + slug = self.kwargs['category_name'] # 从URL获取分类slug + category = get_object_or_404(Category, slug=slug) # 获取分类对象 - categoryname = category.name - self.categoryname = categoryname + categoryname = category.name # 分类名称 + self.categoryname = categoryname # 保存分类名称 categorynames = list( - map(lambda c: c.name, category.get_sub_categorys())) + map(lambda c: c.name, category.get_sub_categorys())) # 获取所有子分类名称 article_list = Article.objects.filter( - category__name__in=categorynames, status='p') + category__name__in=categorynames, status='p') # 过滤分类下的文章 return article_list def get_queryset_cache_key(self): - slug = self.kwargs['category_name'] - category = get_object_or_404(Category, slug=slug) - categoryname = category.name - self.categoryname = categoryname + # 生成分类目录缓存键 + slug = self.kwargs['category_name'] # 分类slug + category = get_object_or_404(Category, slug=slug) # 分类对象 + categoryname = category.name # 分类名称 + self.categoryname = categoryname # 保存分类名称 cache_key = 'category_list_{categoryname}_{page}'.format( - categoryname=categoryname, page=self.page_number) + categoryname=categoryname, page=self.page_number) # 包含分类名和页码的缓存键 return cache_key def get_context_data(self, **kwargs): - + # 获取分类目录页的上下文数据 categoryname = self.categoryname try: - categoryname = categoryname.split('/')[-1] + categoryname = categoryname.split('/')[-1] # 提取分类名称的最后部分 except BaseException: pass - kwargs['page_type'] = CategoryDetailView.page_type - kwargs['tag_name'] = categoryname - return super(CategoryDetailView, self).get_context_data(**kwargs) + kwargs['page_type'] = CategoryDetailView.page_type # 页面类型 + kwargs['tag_name'] = categoryname # 分类名称 + return super(CategoryDetailView, self).get_context_data(**kwargs) # 调用父类方法 class AuthorDetailView(ArticleListView): ''' - 作者详情页 + 作者详情页视图 ''' - page_type = '作者文章归档' + page_type = '作者文章归档' # 页面类型 def get_queryset_cache_key(self): + # 生成作者详情页缓存键 from uuslug import slugify - author_name = slugify(self.kwargs['author_name']) + author_name = slugify(self.kwargs['author_name']) # 作者名称slug化 cache_key = 'author_{author_name}_{page}'.format( - author_name=author_name, page=self.page_number) + author_name=author_name, page=self.page_number) # 包含作者名和页码的缓存键 return cache_key def get_queryset_data(self): - author_name = self.kwargs['author_name'] + # 获取作者文章数据 + author_name = self.kwargs['author_name'] # 作者名称 article_list = Article.objects.filter( - author__username=author_name, type='a', status='p') + author__username=author_name, type='a', status='p') # 过滤作者文章 return article_list def get_context_data(self, **kwargs): - author_name = self.kwargs['author_name'] - kwargs['page_type'] = AuthorDetailView.page_type - kwargs['tag_name'] = author_name - return super(AuthorDetailView, self).get_context_data(**kwargs) + # 获取作者详情页的上下文数据 + author_name = self.kwargs['author_name'] # 作者名称 + kwargs['page_type'] = AuthorDetailView.page_type # 页面类型 + kwargs['tag_name'] = author_name # 作者名称 + return super(AuthorDetailView, self).get_context_data(**kwargs) # 调用父类方法 class TagDetailView(ArticleListView): ''' - 标签列表页面 + 标签列表页面视图 ''' - page_type = '分类标签归档' + page_type = '分类标签归档' # 页面类型 def get_queryset_data(self): - slug = self.kwargs['tag_name'] - tag = get_object_or_404(Tag, slug=slug) - tag_name = tag.name - self.name = tag_name + # 获取标签下的文章数据 + slug = self.kwargs['tag_name'] # 标签slug + tag = get_object_or_404(Tag, slug=slug) # 标签对象 + tag_name = tag.name # 标签名称 + self.name = tag_name # 保存标签名称 article_list = Article.objects.filter( - tags__name=tag_name, type='a', status='p') + tags__name=tag_name, type='a', status='p') # 过滤标签文章 return article_list def get_queryset_cache_key(self): - slug = self.kwargs['tag_name'] - tag = get_object_or_404(Tag, slug=slug) - tag_name = tag.name - self.name = tag_name + # 生成标签详情页缓存键 + slug = self.kwargs['tag_name'] # 标签slug + tag = get_object_or_404(Tag, slug=slug) # 标签对象 + tag_name = tag.name # 标签名称 + self.name = tag_name # 保存标签名称 cache_key = 'tag_{tag_name}_{page}'.format( - tag_name=tag_name, page=self.page_number) + tag_name=tag_name, page=self.page_number) # 包含标签名和页码的缓存键 return cache_key def get_context_data(self, **kwargs): - # tag_name = self.kwargs['tag_name'] - tag_name = self.name - kwargs['page_type'] = TagDetailView.page_type - kwargs['tag_name'] = tag_name - return super(TagDetailView, self).get_context_data(**kwargs) + # 获取标签详情页的上下文数据 + tag_name = self.name # 标签名称 + kwargs['page_type'] = TagDetailView.page_type # 页面类型 + kwargs['tag_name'] = tag_name # 标签名称 + return super(TagDetailView, self).get_context_data(**kwargs) # 调用父类方法 class ArchivesView(ArticleListView): ''' - 文章归档页面 + 文章归档页面视图 ''' - page_type = '文章归档' - paginate_by = None - page_kwarg = None - template_name = 'blog/article_archives.html' + page_type = '文章归档' # 页面类型 + paginate_by = None # 不分页 + page_kwarg = None # 无页码参数 + template_name = 'blog/article_archives.html' # 归档页面模板 def get_queryset_data(self): - return Article.objects.filter(status='p').all() + # 获取所有文章数据 + return Article.objects.filter(status='p').all() # 所有已发布文章 def get_queryset_cache_key(self): - cache_key = 'archives' + # 生成归档页面缓存键 + cache_key = 'archives' # 固定缓存键 return cache_key class LinkListView(ListView): - model = Links - template_name = 'blog/links_list.html' + # 友情链接列表视图 + model = Links # 关联的模型 + template_name = 'blog/links_list.html' # 模板名称 def get_queryset(self): - return Links.objects.filter(is_enable=True) + # 获取启用的友情链接 + return Links.objects.filter(is_enable=True) # 过滤启用的链接 class EsSearchView(SearchView): + # Elasticsearch搜索视图 def get_context(self): - paginator, page = self.build_page() + # 获取搜索结果的上下文数据 + paginator, page = self.build_page() # 构建分页 context = { - "query": self.query, - "form": self.form, - "page": page, - "paginator": paginator, - "suggestion": None, + "query": self.query, # 搜索查询 + "form": self.form, # 搜索表单 + "page": page, # 当前页 + "paginator": paginator, # 分页器 + "suggestion": None, # 搜索建议 } if hasattr(self.results, "query") and self.results.query.backend.include_spelling: - context["suggestion"] = self.results.query.get_spelling_suggestion() - context.update(self.extra_context()) + context["suggestion"] = self.results.query.get_spelling_suggestion() # 获取拼写建议 + context.update(self.extra_context()) # 更新额外上下文 return context -@csrf_exempt +@csrf_exempt # 免除CSRF保护 def fileupload(request): """ - 该方法需自己写调用端来上传图片,该方法仅提供图床功能 + 文件上传方法,提供图床功能 :param request: :return: """ - if request.method == 'POST': - sign = request.GET.get('sign', None) - if not sign: - return HttpResponseForbidden() - if not sign == get_sha256(get_sha256(settings.SECRET_KEY)): - return HttpResponseForbidden() - response = [] - for filename in request.FILES: - timestr = timezone.now().strftime('%Y/%m/%d') - imgextensions = ['jpg', 'png', 'jpeg', 'bmp'] - fname = u''.join(str(filename)) - isimage = len([i for i in imgextensions if fname.find(i) >= 0]) > 0 - base_dir = os.path.join(settings.STATICFILES, "files" if not isimage else "image", timestr) - if not os.path.exists(base_dir): - os.makedirs(base_dir) - savepath = os.path.normpath(os.path.join(base_dir, f"{uuid.uuid4().hex}{os.path.splitext(filename)[-1]}")) - if not savepath.startswith(base_dir): + if request.method == 'POST': # 只处理POST请求 + sign = request.GET.get('sign', None) # 获取签名 + if not sign: # 验证签名是否存在 + return HttpResponseForbidden() # 无签名返回403 + if not sign == get_sha256(get_sha256(settings.SECRET_KEY)): # 验证签名是否正确 + return HttpResponseForbidden() # 签名错误返回403 + response = [] # 响应列表 + for filename in request.FILES: # 遍历所有上传的文件 + timestr = timezone.now().strftime('%Y/%m/%d') # 时间目录 + imgextensions = ['jpg', 'png', 'jpeg', 'bmp'] # 图片扩展名 + fname = u''.join(str(filename)) # 文件名 + isimage = len([i for i in imgextensions if fname.find(i) >= 0]) > 0 # 判断是否为图片 + base_dir = os.path.join(settings.STATICFILES, "files" if not isimage else "image", timestr) # 基础目录 + if not os.path.exists(base_dir): # 如果目录不存在 + os.makedirs(base_dir) # 创建目录 + savepath = os.path.normpath(os.path.join(base_dir, f"{uuid.uuid4().hex}{os.path.splitext(filename)[-1]}")) # 保存路径 + if not savepath.startswith(base_dir): # 路径安全检查 return HttpResponse("only for post") - with open(savepath, 'wb+') as wfile: + with open(savepath, 'wb+') as wfile: # 写入文件 for chunk in request.FILES[filename].chunks(): wfile.write(chunk) - if isimage: + if isimage: # 如果是图片 from PIL import Image - image = Image.open(savepath) - image.save(savepath, quality=20, optimize=True) - url = static(savepath) - response.append(url) - return HttpResponse(response) + image = Image.open(savepath) # 打开图片 + image.save(savepath, quality=20, optimize=True) # 压缩图片 + url = static(savepath) # 生成静态文件URL + response.append(url) # 添加到响应列表 + return HttpResponse(response) # 返回URL列表 else: - return HttpResponse("only for post") + return HttpResponse("only for post") # 非POST请求返回提示 def page_not_found_view( request, exception, template_name='blog/error_page.html'): + # 404错误页面视图 if exception: - logger.error(exception) - url = request.get_full_path() + logger.error(exception) # 记录错误日志 + url = request.get_full_path() # 获取请求URL return render(request, template_name, {'message': _('Sorry, the page you requested is not found, please click the home page to see other?'), - 'statuscode': '404'}, - status=404) + 'statuscode': '404'}, # 错误信息 + status=404) # 返回404状态码 def server_error_view(request, template_name='blog/error_page.html'): + # 500错误页面视图 return render(request, template_name, {'message': _('Sorry, the server is busy, please click the home page to see other?'), - 'statuscode': '500'}, - status=500) + 'statuscode': '500'}, # 错误信息 + status=500) # 返回500状态码 def permission_denied_view( request, exception, template_name='blog/error_page.html'): + # 403错误页面视图 if exception: - logger.error(exception) + logger.error(exception) # 记录错误日志 return render( request, template_name, { 'message': _('Sorry, you do not have permission to access this page?'), - 'statuscode': '403'}, status=403) + 'statuscode': '403'}, status=403) # 返回403状态码 def clean_cache_view(request): - cache.clear() - return HttpResponse('ok') + # 清理缓存视图 + cache.clear() # 清除所有缓存 + return HttpResponse('ok') # 返回成功响应 \ No newline at end of file diff --git a/src/DjangoBlog/comments/admin.py b/src/DjangoBlog/comments/admin.py index dbde14f..a243fc7 100644 --- a/src/DjangoBlog/comments/admin.py +++ b/src/DjangoBlog/comments/admin.py @@ -5,45 +5,50 @@ from django.utils.translation import gettext_lazy as _ def disable_commentstatus(modeladmin, request, queryset): - queryset.update(is_enable=False) + # 禁用选中评论的动作 + queryset.update(is_enable=False) # 将is_enable字段设置为False def enable_commentstatus(modeladmin, request, queryset): - queryset.update(is_enable=True) + # 启用选中评论的动作 + queryset.update(is_enable=True) # 将is_enable字段设置为True -disable_commentstatus.short_description = _('Disable comments') -enable_commentstatus.short_description = _('Enable comments') +disable_commentstatus.short_description = _('Disable comments') # 动作显示名称 +enable_commentstatus.short_description = _('Enable comments') # 动作显示名称 class CommentAdmin(admin.ModelAdmin): - list_per_page = 20 + # 评论管理类 + list_per_page = 20 # 每页显示20条记录 list_display = ( 'id', 'body', - 'link_to_userinfo', - 'link_to_article', + 'link_to_userinfo', # 自定义用户信息链接字段 + 'link_to_article', # 自定义文章链接字段 'is_enable', - 'creation_time') - list_display_links = ('id', 'body', 'is_enable') - list_filter = ('is_enable',) - exclude = ('creation_time', 'last_modify_time') - actions = [disable_commentstatus, enable_commentstatus] - raw_id_fields = ('author', 'article') - search_fields = ('body',) + 'creation_time') # 列表页显示的字段 + list_display_links = ('id', 'body', 'is_enable') # 可点击链接的字段 + list_filter = ('is_enable',) # 右侧过滤器 + exclude = ('creation_time', 'last_modify_time') # 排除的字段 + actions = [disable_commentstatus, enable_commentstatus] # 管理员动作列表 + raw_id_fields = ('author', 'article') # 使用原始ID字段(外键搜索) + search_fields = ('body',) # 搜索字段 def link_to_userinfo(self, obj): - info = (obj.author._meta.app_label, obj.author._meta.model_name) - link = reverse('admin:%s_%s_change' % info, args=(obj.author.id,)) + # 创建用户信息链接的自定义方法 + info = (obj.author._meta.app_label, obj.author._meta.model_name) # 获取用户模型信息 + link = reverse('admin:%s_%s_change' % info, args=(obj.author.id,)) # 生成用户编辑链接 return format_html( u'%s' % - (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): - info = (obj.article._meta.app_label, obj.article._meta.model_name) - link = reverse('admin:%s_%s_change' % info, args=(obj.article.id,)) + # 创建文章链接的自定义方法 + info = (obj.article._meta.app_label, obj.article._meta.model_name) # 获取文章模型信息 + link = reverse('admin:%s_%s_change' % info, args=(obj.article.id,)) # 生成文章编辑链接 return format_html( - u'%s' % (link, obj.article.title)) + u'%s' % (link, obj.article.title)) # 显示文章标题 - link_to_userinfo.short_description = _('User') - link_to_article.short_description = _('Article') + link_to_userinfo.short_description = _('User') # 字段显示名称 + link_to_article.short_description = _('Article') # 字段显示名称 \ No newline at end of file diff --git a/src/DjangoBlog/comments/apps.py b/src/DjangoBlog/comments/apps.py index ff01b77..99b13a8 100644 --- a/src/DjangoBlog/comments/apps.py +++ b/src/DjangoBlog/comments/apps.py @@ -2,4 +2,4 @@ from django.apps import AppConfig class CommentsConfig(AppConfig): - name = 'comments' + name = 'comments' # 指定应用的Python路径为'comments' \ No newline at end of file diff --git a/src/DjangoBlog/comments/forms.py b/src/DjangoBlog/comments/forms.py index e83737d..5b258e5 100644 --- a/src/DjangoBlog/comments/forms.py +++ b/src/DjangoBlog/comments/forms.py @@ -5,9 +5,10 @@ from .models import Comment class CommentForm(ModelForm): + # 父评论ID字段,使用隐藏输入框,非必填 parent_comment_id = forms.IntegerField( widget=forms.HiddenInput, required=False) class Meta: - model = Comment - fields = ['body'] + model = Comment # 指定模型为Comment + fields = ['body'] # 表单只包含body字段 \ No newline at end of file diff --git a/src/DjangoBlog/comments/migrations/0001_initial.py b/src/DjangoBlog/comments/migrations/0001_initial.py index 61d1e53..47b78cd 100644 --- a/src/DjangoBlog/comments/migrations/0001_initial.py +++ b/src/DjangoBlog/comments/migrations/0001_initial.py @@ -8,31 +8,32 @@ import django.utils.timezone class Migration(migrations.Migration): - initial = True + initial = True # 标记为初始迁移 dependencies = [ - ('blog', '0001_initial'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('blog', '0001_initial'), # 依赖blog应用的初始迁移 + migrations.swappable_dependency(settings.AUTH_USER_MODEL), # 依赖用户模型 ] operations = [ + # 创建评论表 migrations.CreateModel( name='Comment', fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('body', models.TextField(max_length=300, verbose_name='正文')), - ('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')), - ('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')), - ('is_enable', models.BooleanField(default=True, verbose_name='是否显示')), - ('article', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.article', verbose_name='文章')), - ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='作者')), - ('parent_comment', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='comments.comment', verbose_name='上级评论')), + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), # 自增主键 + ('body', models.TextField(max_length=300, verbose_name='正文')), # 评论正文,最大长度300 + ('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')), # 创建时间 + ('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')), # 最后修改时间 + ('is_enable', models.BooleanField(default=True, verbose_name='是否显示')), # 是否启用显示 + ('article', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.article', verbose_name='文章')), # 关联文章外键,级联删除 + ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='作者')), # 关联作者外键,级联删除 + ('parent_comment', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='comments.comment', verbose_name='上级评论')), # 自关联上级评论,可为空 ], options={ - 'verbose_name': '评论', - 'verbose_name_plural': '评论', - 'ordering': ['-id'], - 'get_latest_by': 'id', + 'verbose_name': '评论', # 单数显示名称 + 'verbose_name_plural': '评论', # 复数显示名称 + 'ordering': ['-id'], # 按ID倒序排列 + 'get_latest_by': 'id', # 指定获取最新记录的字段 }, ), - ] + ] \ No newline at end of file diff --git a/src/DjangoBlog/comments/migrations/0002_alter_comment_is_enable.py b/src/DjangoBlog/comments/migrations/0002_alter_comment_is_enable.py index 17c44db..2e83f07 100644 --- a/src/DjangoBlog/comments/migrations/0002_alter_comment_is_enable.py +++ b/src/DjangoBlog/comments/migrations/0002_alter_comment_is_enable.py @@ -6,13 +6,14 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('comments', '0001_initial'), + ('comments', '0001_initial'), # 依赖comments应用的初始迁移 ] operations = [ + # 修改Comment模型的is_enable字段默认值 migrations.AlterField( model_name='comment', name='is_enable', - field=models.BooleanField(default=False, verbose_name='是否显示'), + field=models.BooleanField(default=False, verbose_name='是否显示'), # 默认值从True改为False ), - ] + ] \ No newline at end of file diff --git a/src/DjangoBlog/comments/migrations/0003_alter_comment_options_remove_comment_created_time_and_more.py b/src/DjangoBlog/comments/migrations/0003_alter_comment_options_remove_comment_created_time_and_more.py index a1ca970..a30344e 100644 --- a/src/DjangoBlog/comments/migrations/0003_alter_comment_options_remove_comment_created_time_and_more.py +++ b/src/DjangoBlog/comments/migrations/0003_alter_comment_options_remove_comment_created_time_and_more.py @@ -9,52 +9,61 @@ import django.utils.timezone class Migration(migrations.Migration): dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('blog', '0005_alter_article_options_alter_category_options_and_more'), - ('comments', '0002_alter_comment_is_enable'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), # 依赖用户模型 + ('blog', '0005_alter_article_options_alter_category_options_and_more'), # 依赖blog应用的第五个迁移 + ('comments', '0002_alter_comment_is_enable'), # 依赖comments应用的第二个迁移 ] operations = [ + # 修改Comment模型的元数据选项(国际化) migrations.AlterModelOptions( name='comment', options={'get_latest_by': 'id', 'ordering': ['-id'], 'verbose_name': 'comment', 'verbose_name_plural': 'comment'}, ), + # 删除created_time字段 migrations.RemoveField( model_name='comment', name='created_time', ), + # 删除last_mod_time字段 migrations.RemoveField( model_name='comment', name='last_mod_time', ), + # 添加creation_time字段 migrations.AddField( model_name='comment', name='creation_time', field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'), ), + # 添加last_modify_time字段 migrations.AddField( model_name='comment', name='last_modify_time', field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='last modify time'), ), + # 修改article字段的verbose_name(国际化) migrations.AlterField( model_name='comment', name='article', field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.article', verbose_name='article'), ), + # 修改author字段的verbose_name(国际化) migrations.AlterField( model_name='comment', name='author', field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='author'), ), + # 修改is_enable字段的verbose_name(国际化) migrations.AlterField( model_name='comment', name='is_enable', field=models.BooleanField(default=False, verbose_name='enable'), ), + # 修改parent_comment字段的verbose_name(国际化) migrations.AlterField( model_name='comment', name='parent_comment', 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/comments/models.py b/src/DjangoBlog/comments/models.py index 7c3bbc8..3429fa4 100644 --- a/src/DjangoBlog/comments/models.py +++ b/src/DjangoBlog/comments/models.py @@ -9,31 +9,33 @@ from blog.models import Article # Create your models here. class Comment(models.Model): - body = models.TextField('正文', max_length=300) - creation_time = models.DateTimeField(_('creation time'), default=now) - last_modify_time = models.DateTimeField(_('last modify time'), default=now) + # 评论模型 + body = models.TextField('正文', max_length=300) # 评论正文,最大长度300字符 + creation_time = models.DateTimeField(_('creation time'), default=now) # 创建时间 + last_modify_time = models.DateTimeField(_('last modify time'), default=now) # 最后修改时间 author = models.ForeignKey( settings.AUTH_USER_MODEL, verbose_name=_('author'), - on_delete=models.CASCADE) + on_delete=models.CASCADE) # 关联作者,级联删除 article = models.ForeignKey( Article, verbose_name=_('article'), - on_delete=models.CASCADE) + on_delete=models.CASCADE) # 关联文章,级联删除 parent_comment = models.ForeignKey( 'self', verbose_name=_('parent comment'), blank=True, null=True, - on_delete=models.CASCADE) + on_delete=models.CASCADE) # 自关联父级评论,可为空 is_enable = models.BooleanField(_('enable'), - default=False, blank=False, null=False) + default=False, blank=False, null=False) # 是否启用,默认禁用 class Meta: - ordering = ['-id'] - verbose_name = _('comment') - verbose_name_plural = verbose_name - get_latest_by = 'id' + ordering = ['-id'] # 按ID倒序排列 + verbose_name = _('comment') # 单数显示名称 + verbose_name_plural = verbose_name # 复数显示名称 + get_latest_by = 'id' # 指定获取最新记录的字段 def __str__(self): - return self.body + # 对象的字符串表示 + return self.body # 返回评论正文 \ No newline at end of file diff --git a/src/DjangoBlog/comments/tests.py b/src/DjangoBlog/comments/tests.py index 2a7f55f..eabd95c 100644 --- a/src/DjangoBlog/comments/tests.py +++ b/src/DjangoBlog/comments/tests.py @@ -12,68 +12,71 @@ from djangoblog.utils import get_max_articleid_commentid class CommentsTest(TransactionTestCase): def setUp(self): - self.client = Client() - self.factory = RequestFactory() + # 测试初始化设置 + self.client = Client() # 创建测试客户端 + self.factory = RequestFactory() # 创建请求工厂 from blog.models import BlogSettings - value = BlogSettings() - value.comment_need_review = True + value = BlogSettings() # 创建博客设置 + value.comment_need_review = True # 设置评论需要审核 value.save() self.user = BlogUser.objects.create_superuser( email="liangliangyy1@gmail.com", username="liangliangyy1", - password="liangliangyy1") + password="liangliangyy1") # 创建超级用户 def update_article_comment_status(self, article): - comments = article.comment_set.all() + # 更新文章评论状态为启用 + comments = article.comment_set.all() # 获取文章的所有评论 for comment in comments: - comment.is_enable = True + comment.is_enable = True # 启用评论 comment.save() def test_validate_comment(self): - self.client.login(username='liangliangyy1', password='liangliangyy1') + # 测试评论验证功能 + self.client.login(username='liangliangyy1', password='liangliangyy1') # 登录用户 - category = Category() - category.name = "categoryccc" + category = Category() # 创建分类 + category.name = "categoryccc" # 设置分类名称 category.save() - article = Article() - article.title = "nicetitleccc" - article.body = "nicecontentccc" - article.author = self.user - article.category = category - article.type = 'a' - article.status = 'p' + article = Article() # 创建文章 + article.title = "nicetitleccc" # 设置文章标题 + article.body = "nicecontentccc" # 设置文章内容 + article.author = self.user # 设置作者 + article.category = category # 设置分类 + article.type = 'a' # 设置类型为文章 + article.status = 'p' # 设置状态为已发布 article.save() comment_url = reverse( 'comments:postcomment', kwargs={ - 'article_id': article.id}) + 'article_id': article.id}) # 生成评论提交URL response = self.client.post(comment_url, { - 'body': '123ffffffffff' + 'body': '123ffffffffff' # 提交评论内容 }) - self.assertEqual(response.status_code, 302) + self.assertEqual(response.status_code, 302) # 验证重定向响应 - article = Article.objects.get(pk=article.pk) - self.assertEqual(len(article.comment_list()), 0) - self.update_article_comment_status(article) + article = Article.objects.get(pk=article.pk) # 重新获取文章 + self.assertEqual(len(article.comment_list()), 0) # 验证评论数为0(需要审核) + self.update_article_comment_status(article) # 更新评论状态为启用 - self.assertEqual(len(article.comment_list()), 1) + self.assertEqual(len(article.comment_list()), 1) # 验证评论数变为1 response = self.client.post(comment_url, { - 'body': '123ffffffffff', + 'body': '123ffffffffff', # 提交第二条评论 }) - self.assertEqual(response.status_code, 302) + self.assertEqual(response.status_code, 302) # 验证重定向响应 - article = Article.objects.get(pk=article.pk) - self.update_article_comment_status(article) - self.assertEqual(len(article.comment_list()), 2) - parent_comment_id = article.comment_list()[0].id + article = Article.objects.get(pk=article.pk) # 重新获取文章 + self.update_article_comment_status(article) # 更新评论状态为启用 + self.assertEqual(len(article.comment_list()), 2) # 验证评论数变为2 + parent_comment_id = article.comment_list()[0].id # 获取第一条评论的ID作为父评论 response = self.client.post(comment_url, { @@ -89,21 +92,21 @@ class CommentsTest(TransactionTestCase): [ddd](http://www.baidu.com) - ''', - 'parent_comment_id': parent_comment_id + ''', # 提交包含Markdown格式的评论 + 'parent_comment_id': parent_comment_id # 设置父评论ID }) - self.assertEqual(response.status_code, 302) - self.update_article_comment_status(article) - article = Article.objects.get(pk=article.pk) - self.assertEqual(len(article.comment_list()), 3) - comment = Comment.objects.get(id=parent_comment_id) - tree = parse_commenttree(article.comment_list(), comment) - self.assertEqual(len(tree), 1) - data = show_comment_item(comment, True) - self.assertIsNotNone(data) - s = get_max_articleid_commentid() - self.assertIsNotNone(s) + self.assertEqual(response.status_code, 302) # 验证重定向响应 + self.update_article_comment_status(article) # 更新评论状态为启用 + article = Article.objects.get(pk=article.pk) # 重新获取文章 + self.assertEqual(len(article.comment_list()), 3) # 验证评论数变为3 + comment = Comment.objects.get(id=parent_comment_id) # 获取父评论 + tree = parse_commenttree(article.comment_list(), comment) # 解析评论树 + self.assertEqual(len(tree), 1) # 验证评论树长度 + data = show_comment_item(comment, True) # 显示评论项 + self.assertIsNotNone(data) # 验证评论项数据不为空 + s = get_max_articleid_commentid() # 获取最大文章ID和评论ID + self.assertIsNotNone(s) # 验证返回值不为空 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/comments/urls.py b/src/DjangoBlog/comments/urls.py index 7df3fab..f7320a3 100644 --- a/src/DjangoBlog/comments/urls.py +++ b/src/DjangoBlog/comments/urls.py @@ -1,11 +1,11 @@ -from django.urls import path - from . import views -app_name = "comments" +app_name = "comments" # 定义应用的命名空间 + urlpatterns = [ + # 文章评论提交URL path( - 'article//postcomment', - views.CommentPostView.as_view(), - name='postcomment'), -] + 'article//postcomment', # URL模式:文章ID/postcomment + views.CommentPostView.as_view(), # 关联评论提交视图 + name='postcomment'), # URL名称 +] \ No newline at end of file diff --git a/src/DjangoBlog/comments/utils.py b/src/DjangoBlog/comments/utils.py index f01dba7..6a1f025 100644 --- a/src/DjangoBlog/comments/utils.py +++ b/src/DjangoBlog/comments/utils.py @@ -9,20 +9,21 @@ logger = logging.getLogger(__name__) def send_comment_email(comment): - site = get_current_site().domain - subject = _('Thanks for your comment') - article_url = f"https://{site}{comment.article.get_absolute_url()}" + # 发送评论相关邮件 + site = get_current_site().domain # 获取当前站点域名 + subject = _('Thanks for your comment') # 邮件主题 + article_url = f"https://{site}{comment.article.get_absolute_url()}" # 构建文章完整URL html_content = _("""

Thank you very much for your comments on this site

You can visit %(article_title)s to review your comments, Thank you again!
If the link above cannot be opened, please copy this link to your browser. - %(article_url)s""") % {'article_url': article_url, 'article_title': comment.article.title} - tomail = comment.author.email - send_email([tomail], subject, html_content) + %(article_url)s""") % {'article_url': article_url, 'article_title': comment.article.title} # 感谢评论邮件内容 + tomail = comment.author.email # 评论作者的邮箱 + send_email([tomail], subject, html_content) # 发送感谢评论邮件 try: - if comment.parent_comment: + if comment.parent_comment: # 如果是对父评论的回复 html_content = _("""Your comment on %(article_title)s
has received a reply.
%(comment_body)s
@@ -31,8 +32,8 @@ def send_comment_email(comment): If the link above cannot be opened, please copy this link to your browser. %(article_url)s """) % {'article_url': article_url, 'article_title': comment.article.title, - 'comment_body': comment.parent_comment.body} - tomail = comment.parent_comment.author.email - send_email([tomail], subject, html_content) + 'comment_body': comment.parent_comment.body} # 评论回复通知邮件内容 + tomail = comment.parent_comment.author.email # 父评论作者的邮箱 + send_email([tomail], subject, html_content) # 发送回复通知邮件 except Exception as e: - logger.error(e) + logger.error(e) # 记录邮件发送错误日志 \ No newline at end of file diff --git a/src/DjangoBlog/comments/views.py b/src/DjangoBlog/comments/views.py index ad9b2b9..173e592 100644 --- a/src/DjangoBlog/comments/views.py +++ b/src/DjangoBlog/comments/views.py @@ -13,51 +13,55 @@ from .models import Comment class CommentPostView(FormView): - form_class = CommentForm - template_name = 'blog/article_detail.html' + # 评论提交视图 + form_class = CommentForm # 使用评论表单 + template_name = 'blog/article_detail.html' # 模板名称 - @method_decorator(csrf_protect) + @method_decorator(csrf_protect) # CSRF保护装饰器 def dispatch(self, *args, **kwargs): - return super(CommentPostView, self).dispatch(*args, **kwargs) + return super(CommentPostView, self).dispatch(*args, **kwargs) # 调用父类dispatch方法 def get(self, request, *args, **kwargs): - article_id = self.kwargs['article_id'] - article = get_object_or_404(Article, pk=article_id) - url = article.get_absolute_url() - return HttpResponseRedirect(url + "#comments") + # 处理GET请求,重定向到文章详情页的评论区域 + article_id = self.kwargs['article_id'] # 获取文章ID + article = get_object_or_404(Article, pk=article_id) # 获取文章对象 + url = article.get_absolute_url() # 获取文章绝对URL + return HttpResponseRedirect(url + "#comments") # 重定向到文章评论区域 def form_invalid(self, form): - article_id = self.kwargs['article_id'] - article = get_object_or_404(Article, pk=article_id) + # 表单验证失败时的处理 + article_id = self.kwargs['article_id'] # 获取文章ID + article = get_object_or_404(Article, pk=article_id) # 获取文章对象 return self.render_to_response({ - 'form': form, - 'article': article + 'form': form, # 返回包含错误信息的表单 + 'article': article # 文章对象 }) def form_valid(self, form): """提交的数据验证合法后的逻辑""" - user = self.request.user - author = BlogUser.objects.get(pk=user.pk) - article_id = self.kwargs['article_id'] - article = get_object_or_404(Article, pk=article_id) - - if article.comment_status == 'c' or article.status == 'c': - raise ValidationError("该文章评论已关闭.") - comment = form.save(False) - comment.article = article + user = self.request.user # 获取当前用户 + author = BlogUser.objects.get(pk=user.pk) # 获取博客用户对象 + article_id = self.kwargs['article_id'] # 获取文章ID + article = get_object_or_404(Article, pk=article_id) # 获取文章对象 + + if article.comment_status == 'c' or article.status == 'c': # 检查评论是否关闭 + raise ValidationError("该文章评论已关闭.") # 抛出验证错误 + + comment = form.save(False) # 创建评论对象但不保存到数据库 + comment.article = article # 设置评论关联的文章 from djangoblog.utils import get_blog_setting - settings = get_blog_setting() - if not settings.comment_need_review: - comment.is_enable = True - comment.author = author + settings = get_blog_setting() # 获取博客设置 + if not settings.comment_need_review: # 如果评论不需要审核 + comment.is_enable = True # 直接启用评论 + comment.author = author # 设置评论作者 - if form.cleaned_data['parent_comment_id']: + if form.cleaned_data['parent_comment_id']: # 如果有父评论ID parent_comment = Comment.objects.get( - pk=form.cleaned_data['parent_comment_id']) - comment.parent_comment = parent_comment + pk=form.cleaned_data['parent_comment_id']) # 获取父评论对象 + comment.parent_comment = parent_comment # 设置父评论 - comment.save(True) + comment.save(True) # 保存评论到数据库 return HttpResponseRedirect( "%s#div-comment-%d" % - (article.get_absolute_url(), comment.pk)) + (article.get_absolute_url(), comment.pk)) # 重定向到新评论的位置 \ No newline at end of file