代码注释

yxt_branch
yxt 2 months ago
parent 4028036a88
commit 7a78ed45a5

@ -9,15 +9,17 @@ from .models import BlogUser
class BlogUserCreationForm(forms.ModelForm): class BlogUserCreationForm(forms.ModelForm):
# 密码输入字段1
password1 = forms.CharField(label=_('password'), widget=forms.PasswordInput) password1 = forms.CharField(label=_('password'), widget=forms.PasswordInput)
# 密码确认字段2
password2 = forms.CharField(label=_('Enter password again'), widget=forms.PasswordInput) password2 = forms.CharField(label=_('Enter password again'), widget=forms.PasswordInput)
class Meta: class Meta:
model = BlogUser model = BlogUser # 指定模型为BlogUser
fields = ('email',) fields = ('email',) # 只包含email字段
def clean_password2(self): def clean_password2(self):
# Check that the two password entries match # 验证两次输入的密码是否一致
password1 = self.cleaned_data.get("password1") password1 = self.cleaned_data.get("password1")
password2 = self.cleaned_data.get("password2") password2 = self.cleaned_data.get("password2")
if password1 and password2 and password1 != password2: if password1 and password2 and password1 != password2:
@ -25,28 +27,30 @@ class BlogUserCreationForm(forms.ModelForm):
return password2 return password2
def save(self, commit=True): def save(self, commit=True):
# Save the provided password in hashed format # 保存用户,设置密码哈希值
user = super().save(commit=False) user = super().save(commit=False)
user.set_password(self.cleaned_data["password1"]) user.set_password(self.cleaned_data["password1"])
if commit: if commit:
user.source = 'adminsite' user.source = 'adminsite' # 设置用户来源为管理员站点
user.save() user.save()
return user return user
class BlogUserChangeForm(UserChangeForm): class BlogUserChangeForm(UserChangeForm):
class Meta: class Meta:
model = BlogUser model = BlogUser # 指定模型为BlogUser
fields = '__all__' fields = '__all__' # 包含所有字段
field_classes = {'username': UsernameField} field_classes = {'username': UsernameField} # 指定username字段类型
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs) # 调用父类初始化方法
class BlogUserAdmin(UserAdmin): class BlogUserAdmin(UserAdmin):
form = BlogUserChangeForm # 使用自定义的表单类
add_form = BlogUserCreationForm form = BlogUserChangeForm # 修改表单
add_form = BlogUserCreationForm # 添加表单
# 列表页显示的字段
list_display = ( list_display = (
'id', 'id',
'nickname', 'nickname',
@ -55,6 +59,9 @@ class BlogUserAdmin(UserAdmin):
'last_login', 'last_login',
'date_joined', 'date_joined',
'source') 'source')
# 列表页可点击的链接字段
list_display_links = ('id', 'username') list_display_links = ('id', 'username')
# 默认排序字段
ordering = ('-id',) ordering = ('-id',)
search_fields = ('username', 'nickname', 'email') # 搜索字段
search_fields = ('username', 'nickname', 'email')

@ -2,4 +2,4 @@ from django.apps import AppConfig
class AccountsConfig(AppConfig): class AccountsConfig(AppConfig):
name = 'accounts' name = 'accounts' # 指定应用的Python路径为'accounts'

@ -11,8 +11,10 @@ from .models import BlogUser
class LoginForm(AuthenticationForm): class LoginForm(AuthenticationForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(LoginForm, self).__init__(*args, **kwargs) super(LoginForm, self).__init__(*args, **kwargs)
# 设置用户名输入框的HTML属性
self.fields['username'].widget = widgets.TextInput( self.fields['username'].widget = widgets.TextInput(
attrs={'placeholder': "username", "class": "form-control"}) attrs={'placeholder': "username", "class": "form-control"})
# 设置密码输入框的HTML属性
self.fields['password'].widget = widgets.PasswordInput( self.fields['password'].widget = widgets.PasswordInput(
attrs={'placeholder': "password", "class": "form-control"}) attrs={'placeholder': "password", "class": "form-control"})
@ -21,6 +23,7 @@ class RegisterForm(UserCreationForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(RegisterForm, self).__init__(*args, **kwargs) super(RegisterForm, self).__init__(*args, **kwargs)
# 设置各字段输入框的HTML属性
self.fields['username'].widget = widgets.TextInput( self.fields['username'].widget = widgets.TextInput(
attrs={'placeholder': "username", "class": "form-control"}) attrs={'placeholder': "username", "class": "form-control"})
self.fields['email'].widget = widgets.EmailInput( self.fields['email'].widget = widgets.EmailInput(
@ -31,17 +34,19 @@ class RegisterForm(UserCreationForm):
attrs={'placeholder': "repeat password", "class": "form-control"}) attrs={'placeholder': "repeat password", "class": "form-control"})
def clean_email(self): def clean_email(self):
# 验证邮箱是否已存在
email = self.cleaned_data['email'] email = self.cleaned_data['email']
if get_user_model().objects.filter(email=email).exists(): if get_user_model().objects.filter(email=email).exists():
raise ValidationError(_("email already exists")) raise ValidationError(_("email already exists"))
return email return email
class Meta: class Meta:
model = get_user_model() model = get_user_model() # 获取当前用户模型
fields = ("username", "email") fields = ("username", "email") # 表单包含的字段
class ForgetPasswordForm(forms.Form): class ForgetPasswordForm(forms.Form):
# 新密码字段
new_password1 = forms.CharField( new_password1 = forms.CharField(
label=_("New password"), label=_("New password"),
widget=forms.PasswordInput( widget=forms.PasswordInput(
@ -52,6 +57,7 @@ class ForgetPasswordForm(forms.Form):
), ),
) )
# 确认密码字段
new_password2 = forms.CharField( new_password2 = forms.CharField(
label="确认密码", label="确认密码",
widget=forms.PasswordInput( widget=forms.PasswordInput(
@ -62,6 +68,7 @@ class ForgetPasswordForm(forms.Form):
), ),
) )
# 邮箱字段
email = forms.EmailField( email = forms.EmailField(
label='邮箱', label='邮箱',
widget=forms.TextInput( widget=forms.TextInput(
@ -72,6 +79,7 @@ class ForgetPasswordForm(forms.Form):
), ),
) )
# 验证码字段
code = forms.CharField( code = forms.CharField(
label=_('Code'), label=_('Code'),
widget=forms.TextInput( widget=forms.TextInput(
@ -83,15 +91,18 @@ class ForgetPasswordForm(forms.Form):
) )
def clean_new_password2(self): def clean_new_password2(self):
# 验证两次输入的新密码是否一致
password1 = self.data.get("new_password1") password1 = self.data.get("new_password1")
password2 = self.data.get("new_password2") password2 = self.data.get("new_password2")
if password1 and password2 and password1 != password2: if password1 and password2 and password1 != password2:
raise ValidationError(_("passwords do not match")) raise ValidationError(_("passwords do not match"))
# 验证密码强度
password_validation.validate_password(password2) password_validation.validate_password(password2)
return password2 return password2
def clean_email(self): def clean_email(self):
# 验证邮箱是否存在
user_email = self.cleaned_data.get("email") user_email = self.cleaned_data.get("email")
if not BlogUser.objects.filter( if not BlogUser.objects.filter(
email=user_email email=user_email
@ -101,6 +112,7 @@ class ForgetPasswordForm(forms.Form):
return user_email return user_email
def clean_code(self): def clean_code(self):
# 验证验证码是否正确
code = self.cleaned_data.get("code") code = self.cleaned_data.get("code")
error = utils.verify( error = utils.verify(
email=self.cleaned_data.get("email"), email=self.cleaned_data.get("email"),
@ -112,6 +124,7 @@ class ForgetPasswordForm(forms.Form):
class ForgetPasswordCodeForm(forms.Form): class ForgetPasswordCodeForm(forms.Form):
# 忘记密码时的邮箱验证表单
email = forms.EmailField( email = forms.EmailField(
label=_('Email'), label=_('Email'),
) )

@ -8,42 +8,60 @@ import django.utils.timezone
class Migration(migrations.Migration): class Migration(migrations.Migration):
initial = True initial = True # 标记这是初始迁移
dependencies = [ dependencies = [
('auth', '0012_alter_user_first_name_max_length'), ('auth', '0012_alter_user_first_name_max_length'), # 依赖auth应用的迁移
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='BlogUser', name='BlogUser', # 创建自定义用户模型BlogUser
fields=[ fields=[
# 主键ID自增BigAutoField
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
# 密码字段使用Django的密码哈希存储
('password', models.CharField(max_length=128, verbose_name='password')), ('password', models.CharField(max_length=128, verbose_name='password')),
# 最后登录时间,可为空
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), ('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')), ('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')), ('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')), ('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')), ('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')), ('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_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')), ('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')), ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
# 自定义字段:昵称
('nickname', models.CharField(blank=True, max_length=100, verbose_name='昵称')), ('nickname', models.CharField(blank=True, max_length=100, verbose_name='昵称')),
# 自定义字段:创建时间
('created_time', models.DateTimeField(default=django.utils.timezone.now, 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='修改时间')), ('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
# 自定义字段:创建来源
('source', models.CharField(blank=True, max_length=100, 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')), ('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')), ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
], ],
options={ options={
'verbose_name': '用户', 'verbose_name': '用户', # 单数显示名称
'verbose_name_plural': '用户', 'verbose_name_plural': '用户', # 复数显示名称
'ordering': ['-id'], 'ordering': ['-id'], # 默认按ID倒序排列
'get_latest_by': 'id', 'get_latest_by': 'id', # 指定获取最新记录的字段
}, },
managers=[ managers=[
# 使用Django原生的UserManager管理用户对象
('objects', django.contrib.auth.models.UserManager()), ('objects', django.contrib.auth.models.UserManager()),
], ],
), ),
] ]

@ -7,40 +7,47 @@ import django.utils.timezone
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('accounts', '0001_initial'), ('accounts', '0001_initial'), # 依赖accounts应用的初始迁移
] ]
operations = [ operations = [
# 修改BlogUser模型的元数据选项
migrations.AlterModelOptions( migrations.AlterModelOptions(
name='bloguser', name='bloguser',
options={'get_latest_by': 'id', 'ordering': ['-id'], 'verbose_name': 'user', 'verbose_name_plural': 'user'}, options={'get_latest_by': 'id', 'ordering': ['-id'], 'verbose_name': 'user', 'verbose_name_plural': 'user'},
), ),
# 删除created_time字段
migrations.RemoveField( migrations.RemoveField(
model_name='bloguser', model_name='bloguser',
name='created_time', name='created_time',
), ),
# 删除last_mod_time字段
migrations.RemoveField( migrations.RemoveField(
model_name='bloguser', model_name='bloguser',
name='last_mod_time', name='last_mod_time',
), ),
# 新增creation_time字段
migrations.AddField( migrations.AddField(
model_name='bloguser', model_name='bloguser',
name='creation_time', name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'), field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
), ),
# 新增last_modify_time字段
migrations.AddField( migrations.AddField(
model_name='bloguser', model_name='bloguser',
name='last_modify_time', name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='last modify time'), field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='last modify time'),
), ),
# 修改nickname字段的verbose_name
migrations.AlterField( migrations.AlterField(
model_name='bloguser', model_name='bloguser',
name='nickname', name='nickname',
field=models.CharField(blank=True, max_length=100, verbose_name='nick name'), field=models.CharField(blank=True, max_length=100, verbose_name='nick name'),
), ),
# 修改source字段的verbose_name
migrations.AlterField( migrations.AlterField(
model_name='bloguser', model_name='bloguser',
name='source', name='source',
field=models.CharField(blank=True, max_length=100, verbose_name='create source'), field=models.CharField(blank=True, max_length=100, verbose_name='create source'),
), ),
] ]

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

@ -13,35 +13,37 @@ from . import utils
class AccountTest(TestCase): class AccountTest(TestCase):
def setUp(self): def setUp(self):
self.client = Client() # 测试初始化设置
self.factory = RequestFactory() self.client = Client() # 创建测试客户端
self.factory = RequestFactory() # 创建请求工厂
self.blog_user = BlogUser.objects.create_user( self.blog_user = BlogUser.objects.create_user(
username="test", username="test",
email="admin@admin.com", email="admin@admin.com",
password="12345678" password="12345678"
) ) # 创建测试用户
self.new_test = "xxx123--=" self.new_test = "xxx123--=" # 新密码测试字符串
def test_validate_account(self): def test_validate_account(self):
# 测试账户验证功能
site = get_current_site().domain site = get_current_site().domain
user = BlogUser.objects.create_superuser( user = BlogUser.objects.create_superuser(
email="liangliangyy1@gmail.com", email="liangliangyy1@gmail.com",
username="liangliangyy1", username="liangliangyy1",
password="qwer!@#$ggg") password="qwer!@#$ggg") # 创建超级用户
testuser = BlogUser.objects.get(username='liangliangyy1') testuser = BlogUser.objects.get(username='liangliangyy1')
loginresult = self.client.login( loginresult = self.client.login(
username='liangliangyy1', username='liangliangyy1',
password='qwer!@#$ggg') password='qwer!@#$ggg') # 测试登录
self.assertEqual(loginresult, True) self.assertEqual(loginresult, True) # 验证登录成功
response = self.client.get('/admin/') response = self.client.get('/admin/') # 访问管理后台
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200) # 验证访问成功
category = Category() category = Category()
category.name = "categoryaaa" category.name = "categoryaaa"
category.creation_time = timezone.now() category.creation_time = timezone.now()
category.last_modify_time = timezone.now() category.last_modify_time = timezone.now()
category.save() category.save() # 创建测试分类
article = Article() article = Article()
article.title = "nicetitleaaa" article.title = "nicetitleaaa"
@ -50,45 +52,46 @@ class AccountTest(TestCase):
article.category = category article.category = category
article.type = 'a' article.type = 'a'
article.status = 'p' article.status = 'p'
article.save() article.save() # 创建测试文章
response = self.client.get(article.get_admin_url()) response = self.client.get(article.get_admin_url()) # 访问文章管理页面
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200) # 验证访问成功
def test_validate_register(self): def test_validate_register(self):
# 测试用户注册功能
self.assertEquals( self.assertEquals(
0, len( 0, len(
BlogUser.objects.filter( BlogUser.objects.filter(
email='user123@user.com'))) email='user123@user.com'))) # 验证用户不存在
response = self.client.post(reverse('account:register'), { response = self.client.post(reverse('account:register'), {
'username': 'user1233', 'username': 'user1233',
'email': 'user123@user.com', 'email': 'user123@user.com',
'password1': 'password123!q@wE#R$T', 'password1': 'password123!q@wE#R$T',
'password2': 'password123!q@wE#R$T', 'password2': 'password123!q@wE#R$T',
}) }) # 提交注册表单
self.assertEquals( self.assertEquals(
1, len( 1, len(
BlogUser.objects.filter( BlogUser.objects.filter(
email='user123@user.com'))) email='user123@user.com'))) # 验证用户创建成功
user = BlogUser.objects.filter(email='user123@user.com')[0] user = BlogUser.objects.filter(email='user123@user.com')[0]
sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id))) sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id))) # 生成验证签名
path = reverse('accounts:result') path = reverse('accounts:result')
url = '{path}?type=validation&id={id}&sign={sign}'.format( url = '{path}?type=validation&id={id}&sign={sign}'.format(
path=path, id=user.id, sign=sign) path=path, id=user.id, sign=sign) # 构建验证URL
response = self.client.get(url) response = self.client.get(url) # 访问验证页面
self.assertEqual(response.status_code, 200) 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 = BlogUser.objects.filter(email='user123@user.com')[0]
user.is_superuser = True user.is_superuser = True # 设置为超级用户
user.is_staff = True user.is_staff = True # 设置为员工
user.save() user.save()
delete_sidebar_cache() delete_sidebar_cache() # 清除侧边栏缓存
category = Category() category = Category()
category.name = "categoryaaa" category.name = "categoryaaa"
category.creation_time = timezone.now() category.creation_time = timezone.now()
category.last_modify_time = timezone.now() category.last_modify_time = timezone.now()
category.save() category.save() # 创建分类
article = Article() article = Article()
article.category = category article.category = category
@ -98,110 +101,114 @@ class AccountTest(TestCase):
article.type = 'a' article.type = 'a'
article.status = 'p' article.status = 'p'
article.save() article.save() # 创建文章
response = self.client.get(article.get_admin_url()) response = self.client.get(article.get_admin_url()) # 访问文章管理页面
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200) # 验证访问成功
response = self.client.get(reverse('account:logout')) response = self.client.get(reverse('account:logout')) # 退出登录
self.assertIn(response.status_code, [301, 302, 200]) self.assertIn(response.status_code, [301, 302, 200]) # 验证重定向
response = self.client.get(article.get_admin_url()) response = self.client.get(article.get_admin_url()) # 再次访问管理页面
self.assertIn(response.status_code, [301, 302, 200]) self.assertIn(response.status_code, [301, 302, 200]) # 验证被重定向(未登录)
response = self.client.post(reverse('account:login'), { response = self.client.post(reverse('account:login'), {
'username': 'user1233', 'username': 'user1233',
'password': 'password123' 'password': 'password123'
}) }) # 重新登录
self.assertIn(response.status_code, [301, 302, 200]) self.assertIn(response.status_code, [301, 302, 200]) # 验证登录重定向
response = self.client.get(article.get_admin_url()) response = self.client.get(article.get_admin_url()) # 再次访问管理页面
self.assertIn(response.status_code, [301, 302, 200]) self.assertIn(response.status_code, [301, 302, 200]) # 验证访问成功
def test_verify_email_code(self): def test_verify_email_code(self):
# 测试邮箱验证码功能
to_email = "admin@admin.com" to_email = "admin@admin.com"
code = generate_code() code = generate_code() # 生成验证码
utils.set_code(to_email, code) utils.set_code(to_email, code) # 设置验证码
utils.send_verify_email(to_email, code) utils.send_verify_email(to_email, code) # 发送验证邮件
err = utils.verify("admin@admin.com", code) err = utils.verify("admin@admin.com", code) # 验证正确邮箱和验证码
self.assertEqual(err, None) self.assertEqual(err, None) # 验证无错误
err = utils.verify("admin@123.com", code) err = utils.verify("admin@123.com", code) # 验证错误邮箱
self.assertEqual(type(err), str) self.assertEqual(type(err), str) # 验证返回错误信息
def test_forget_password_email_code_success(self): def test_forget_password_email_code_success(self):
# 测试忘记密码邮箱验证码成功情况
resp = self.client.post( resp = self.client.post(
path=reverse("account:forget_password_code"), path=reverse("account:forget_password_code"),
data=dict(email="admin@admin.com") data=dict(email="admin@admin.com")
) ) # 提交忘记密码请求
self.assertEqual(resp.status_code, 200) self.assertEqual(resp.status_code, 200) # 验证响应成功
self.assertEqual(resp.content.decode("utf-8"), "ok") self.assertEqual(resp.content.decode("utf-8"), "ok") # 验证返回内容
def test_forget_password_email_code_fail(self): def test_forget_password_email_code_fail(self):
# 测试忘记密码邮箱验证码失败情况
resp = self.client.post( resp = self.client.post(
path=reverse("account:forget_password_code"), path=reverse("account:forget_password_code"),
data=dict() data=dict()
) ) # 提交空数据
self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱") self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱") # 验证错误信息
resp = self.client.post( resp = self.client.post(
path=reverse("account:forget_password_code"), path=reverse("account:forget_password_code"),
data=dict(email="admin@com") 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): 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( data = dict(
new_password1=self.new_test, new_password1=self.new_test,
new_password2=self.new_test, new_password2=self.new_test,
email=self.blog_user.email, email=self.blog_user.email,
code=code, code=code,
) ) # 准备重置密码数据
resp = self.client.post( resp = self.client.post(
path=reverse("account:forget_password"), path=reverse("account:forget_password"),
data=data data=data
) ) # 提交重置密码请求
self.assertEqual(resp.status_code, 302) self.assertEqual(resp.status_code, 302) # 验证重定向响应
# 验证用户密码是否修改成功 # 验证用户密码是否修改成功
blog_user = BlogUser.objects.filter( blog_user = BlogUser.objects.filter(
email=self.blog_user.email, email=self.blog_user.email,
).first() # type: BlogUser ).first() # type: BlogUser # 获取用户
self.assertNotEqual(blog_user, None) self.assertNotEqual(blog_user, None) # 验证用户存在
self.assertEqual(blog_user.check_password(data["new_password1"]), True) self.assertEqual(blog_user.check_password(data["new_password1"]), True) # 验证密码修改成功
def test_forget_password_email_not_user(self): def test_forget_password_email_not_user(self):
# 测试忘记密码时用户不存在的情况
data = dict( data = dict(
new_password1=self.new_test, new_password1=self.new_test,
new_password2=self.new_test, new_password2=self.new_test,
email="123@123.com", email="123@123.com",
code="123456", code="123456",
) ) # 准备不存在的用户数据
resp = self.client.post( resp = self.client.post(
path=reverse("account:forget_password"), path=reverse("account:forget_password"),
data=data data=data
) ) # 提交重置密码请求
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.status_code, 200) # 验证响应成功(但应该显示错误)
def test_forget_password_email_code_error(self): 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( data = dict(
new_password1=self.new_test, new_password1=self.new_test,
new_password2=self.new_test, new_password2=self.new_test,
email=self.blog_user.email, email=self.blog_user.email,
code="111111", code="111111", # 使用错误验证码
) ) # 准备重置密码数据
resp = self.client.post( resp = self.client.post(
path=reverse("account:forget_password"), path=reverse("account:forget_password"),
data=data data=data
) ) # 提交重置密码请求
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.status_code, 200) # 验证响应成功(但应该显示验证码错误)

@ -4,25 +4,32 @@ from django.urls import re_path
from . import views from . import views
from .forms import LoginForm from .forms import LoginForm
app_name = "accounts" app_name = "accounts" # 定义应用的命名空间
urlpatterns = [re_path(r'^login/$', urlpatterns = [
views.LoginView.as_view(success_url='/'), # 登录URL使用LoginForm作为认证表单成功跳转到首页
name='login', re_path(r'^login/$',
kwargs={'authentication_form': LoginForm}), views.LoginView.as_view(success_url='/'),
re_path(r'^register/$', name='login',
views.RegisterView.as_view(success_url="/"), kwargs={'authentication_form': LoginForm}),
name='register'), # 注册URL注册成功跳转到首页
re_path(r'^logout/$', re_path(r'^register/$',
views.LogoutView.as_view(), views.RegisterView.as_view(success_url="/"),
name='logout'), name='register'),
path(r'account/result.html', # 退出登录URL
views.account_result, re_path(r'^logout/$',
name='result'), views.LogoutView.as_view(),
re_path(r'^forget_password/$', name='logout'),
views.ForgetPasswordView.as_view(), # 账户操作结果页面URL
name='forget_password'), path(r'account/result.html',
re_path(r'^forget_password_code/$', views.account_result,
views.ForgetPasswordEmailCode.as_view(), name='result'),
name='forget_password_code'), # 忘记密码页面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'),
]

@ -8,19 +8,25 @@ class EmailOrUsernameModelBackend(ModelBackend):
""" """
def authenticate(self, request, username=None, password=None, **kwargs): def authenticate(self, request, username=None, password=None, **kwargs):
# 判断输入是否包含@符号,决定使用邮箱还是用户名进行验证
if '@' in username: if '@' in username:
kwargs = {'email': username} kwargs = {'email': username} # 如果包含@,按邮箱处理
else: else:
kwargs = {'username': username} kwargs = {'username': username} # 否则按用户名处理
try: try:
# 根据用户名或邮箱查找用户
user = get_user_model().objects.get(**kwargs) user = get_user_model().objects.get(**kwargs)
# 验证密码是否正确
if user.check_password(password): if user.check_password(password):
return user return user
except get_user_model().DoesNotExist: except get_user_model().DoesNotExist:
# 用户不存在时返回None
return None return None
def get_user(self, username): def get_user(self, username):
try: try:
# 根据用户ID获取用户对象
return get_user_model().objects.get(pk=username) return get_user_model().objects.get(pk=username)
except get_user_model().DoesNotExist: except get_user_model().DoesNotExist:
return None # 用户不存在时返回None
return None

@ -7,7 +7,7 @@ from django.utils.translation import gettext_lazy as _
from djangoblog.utils import send_email 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")): 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 = _( html_content = _(
"You are resetting the password, the verification code is%(code)s, valid within 5 minutes, please keep it " "You are resetting the password, the verification code is%(code)s, valid within 5 minutes, please keep it "
"properly") % {'code': code} "properly") % {'code': code} # 邮件内容模板,包含验证码和有效期提示
send_email([to_mail], subject, html_content) send_email([to_mail], subject, html_content) # 调用发送邮件函数
def verify(email: str, code: str) -> typing.Optional[str]: def verify(email: str, code: str) -> typing.Optional[str]:
@ -34,16 +34,16 @@ def verify(email: str, code: str) -> typing.Optional[str]:
这里的错误处理不太合理应该采用raise抛出 这里的错误处理不太合理应该采用raise抛出
否测调用方也需要对error进行处理 否测调用方也需要对error进行处理
""" """
cache_code = get_code(email) cache_code = get_code(email) # 从缓存获取该邮箱对应的验证码
if cache_code != code: if cache_code != code: # 比较缓存中的验证码和用户输入的验证码
return gettext("Verification code error") return gettext("Verification code error") # 验证码不匹配时返回错误信息
def set_code(email: str, code: str): def set_code(email: str, code: str):
"""设置code""" """设置code"""
cache.set(email, code, _code_ttl.seconds) cache.set(email, code, _code_ttl.seconds) # 将验证码存入缓存设置过期时间为5分钟
def get_code(email: str) -> typing.Optional[str]: def get_code(email: str) -> typing.Optional[str]:
"""获取code""" """获取code"""
return cache.get(email) return cache.get(email) # 从缓存获取指定邮箱的验证码

@ -32,27 +32,27 @@ logger = logging.getLogger(__name__)
# Create your views here. # Create your views here.
class RegisterView(FormView): class RegisterView(FormView):
form_class = RegisterForm form_class = RegisterForm # 使用注册表单
template_name = 'account/registration_form.html' template_name = 'account/registration_form.html' # 注册模板
@method_decorator(csrf_protect) @method_decorator(csrf_protect) # CSRF保护装饰器
def dispatch(self, *args, **kwargs): def dispatch(self, *args, **kwargs):
return super(RegisterView, self).dispatch(*args, **kwargs) return super(RegisterView, self).dispatch(*args, **kwargs)
def form_valid(self, form): def form_valid(self, form):
if form.is_valid(): if form.is_valid():
user = form.save(False) user = form.save(False) # 不立即保存用户
user.is_active = False user.is_active = False # 设置用户为非活跃状态
user.source = 'Register' user.source = 'Register' # 设置用户来源为注册
user.save(True) user.save(True) # 保存用户到数据库
site = get_current_site().domain site = get_current_site().domain # 获取当前站点域名
sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id))) sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id))) # 生成验证签名
if settings.DEBUG: if settings.DEBUG:
site = '127.0.0.1:8000' site = '127.0.0.1:8000' # 调试模式下使用本地地址
path = reverse('account:result') path = reverse('account:result')
url = "http://{site}{path}?type=validation&id={id}&sign={sign}".format( url = "http://{site}{path}?type=validation&id={id}&sign={sign}".format(
site=site, path=path, id=user.id, sign=sign) site=site, path=path, id=user.id, sign=sign) # 构建验证URL
content = """ content = """
<p>请点击下面链接验证您的邮箱</p> <p>请点击下面链接验证您的邮箱</p>
@ -63,142 +63,139 @@ class RegisterView(FormView):
<br /> <br />
如果上面链接无法打开请将此链接复制至浏览器 如果上面链接无法打开请将此链接复制至浏览器
{url} {url}
""".format(url=url) """.format(url=url) # 邮件内容模板
send_email( send_email(
emailto=[ emailto=[
user.email, user.email,
], ],
title='验证您的电子邮箱', title='验证您的电子邮箱',
content=content) content=content) # 发送验证邮件
url = reverse('accounts:result') + \ url = reverse('accounts:result') + \
'?type=register&id=' + str(user.id) '?type=register&id=' + str(user.id) # 构建注册结果URL
return HttpResponseRedirect(url) return HttpResponseRedirect(url) # 重定向到结果页面
else: else:
return self.render_to_response({ return self.render_to_response({
'form': form 'form': form # 表单验证失败,重新渲染表单
}) })
class LogoutView(RedirectView): class LogoutView(RedirectView):
url = '/login/' url = '/login/' # 退出后重定向到登录页
@method_decorator(never_cache) @method_decorator(never_cache) # 禁止缓存装饰器
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
return super(LogoutView, self).dispatch(request, *args, **kwargs) return super(LogoutView, self).dispatch(request, *args, **kwargs)
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
logout(request) logout(request) # 执行退出登录
delete_sidebar_cache() delete_sidebar_cache() # 删除侧边栏缓存
return super(LogoutView, self).get(request, *args, **kwargs) return super(LogoutView, self).get(request, *args, **kwargs) # 重定向到登录页
class LoginView(FormView): class LoginView(FormView):
form_class = LoginForm form_class = LoginForm # 使用登录表单
template_name = 'account/login.html' template_name = 'account/login.html' # 登录模板
success_url = '/' success_url = '/' # 登录成功默认跳转首页
redirect_field_name = REDIRECT_FIELD_NAME redirect_field_name = REDIRECT_FIELD_NAME # 重定向字段名
login_ttl = 2626560 # 一个月的时间 login_ttl = 2626560 # 一个月的时间(会话有效期)
@method_decorator(sensitive_post_parameters('password')) @method_decorator(sensitive_post_parameters('password')) # 敏感参数保护
@method_decorator(csrf_protect) @method_decorator(csrf_protect) # CSRF保护
@method_decorator(never_cache) @method_decorator(never_cache) # 禁止缓存
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
return super(LoginView, self).dispatch(request, *args, **kwargs) return super(LoginView, self).dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
redirect_to = self.request.GET.get(self.redirect_field_name) redirect_to = self.request.GET.get(self.redirect_field_name) # 获取重定向URL
if redirect_to is None: if redirect_to is None:
redirect_to = '/' redirect_to = '/' # 默认重定向到首页
kwargs['redirect_to'] = redirect_to kwargs['redirect_to'] = redirect_to
return super(LoginView, self).get_context_data(**kwargs) return super(LoginView, self).get_context_data(**kwargs) # 返回上下文数据
def form_valid(self, form): def form_valid(self, form):
form = AuthenticationForm(data=self.request.POST, request=self.request) form = AuthenticationForm(data=self.request.POST, request=self.request) # 使用Django认证表单
if form.is_valid(): if form.is_valid():
delete_sidebar_cache() delete_sidebar_cache() # 删除侧边栏缓存
logger.info(self.redirect_field_name) logger.info(self.redirect_field_name)
auth.login(self.request, form.get_user()) auth.login(self.request, form.get_user()) # 执行登录操作
if self.request.POST.get("remember"): if self.request.POST.get("remember"): # 检查"记住我"选项
self.request.session.set_expiry(self.login_ttl) self.request.session.set_expiry(self.login_ttl) # 设置会话有效期
return super(LoginView, self).form_valid(form) return super(LoginView, self).form_valid(form) # 调用父类form_valid方法
# return HttpResponseRedirect('/')
else: else:
return self.render_to_response({ return self.render_to_response({
'form': form 'form': form # 表单验证失败,重新渲染表单
}) })
def get_success_url(self): def get_success_url(self):
redirect_to = self.request.POST.get(self.redirect_field_name) # 从POST获取重定向URL
redirect_to = self.request.POST.get(self.redirect_field_name)
if not url_has_allowed_host_and_scheme( if not url_has_allowed_host_and_scheme(
url=redirect_to, allowed_hosts=[ url=redirect_to, allowed_hosts=[
self.request.get_host()]): self.request.get_host()]): # 验证URL安全性
redirect_to = self.success_url redirect_to = self.success_url # 使用默认成功URL
return redirect_to return redirect_to # 返回重定向URL
def account_result(request): def account_result(request):
type = request.GET.get('type') type = request.GET.get('type') # 获取操作类型
id = request.GET.get('id') id = request.GET.get('id') # 获取用户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) logger.info(type)
if user.is_active: if user.is_active: # 如果用户已激活,重定向到首页
return HttpResponseRedirect('/') return HttpResponseRedirect('/')
if type and type in ['register', 'validation']: if type and type in ['register', 'validation']: # 检查操作类型
if type == 'register': if type == 'register':
content = ''' content = '''
恭喜您注册成功一封验证邮件已经发送到您的邮箱请验证您的邮箱后登录本站 恭喜您注册成功一封验证邮件已经发送到您的邮箱请验证您的邮箱后登录本站
''' ''' # 注册成功提示
title = '注册成功' title = '注册成功'
else: else:
c_sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id))) c_sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id))) # 计算验证签名
sign = request.GET.get('sign') sign = request.GET.get('sign') # 获取请求中的签名
if sign != c_sign: if sign != c_sign: # 验证签名是否匹配
return HttpResponseForbidden() return HttpResponseForbidden() # 签名不匹配返回403
user.is_active = True user.is_active = True # 激活用户
user.save() user.save() # 保存用户状态
content = ''' content = '''
恭喜您已经成功的完成邮箱验证您现在可以使用您的账号来登录本站 恭喜您已经成功的完成邮箱验证您现在可以使用您的账号来登录本站
''' ''' # 验证成功提示
title = '验证成功' title = '验证成功'
return render(request, 'account/result.html', { return render(request, 'account/result.html', {
'title': title, 'title': title,
'content': content 'content': content # 渲染结果页面
}) })
else: else:
return HttpResponseRedirect('/') return HttpResponseRedirect('/') # 无效类型重定向到首页
class ForgetPasswordView(FormView): class ForgetPasswordView(FormView):
form_class = ForgetPasswordForm form_class = ForgetPasswordForm # 使用忘记密码表单
template_name = 'account/forget_password.html' template_name = 'account/forget_password.html' # 忘记密码模板
def form_valid(self, form): def form_valid(self, form):
if form.is_valid(): if form.is_valid():
blog_user = BlogUser.objects.filter(email=form.cleaned_data.get("email")).get() blog_user = BlogUser.objects.filter(email=form.cleaned_data.get("email")).get() # 根据邮箱获取用户
blog_user.password = make_password(form.cleaned_data["new_password2"]) blog_user.password = make_password(form.cleaned_data["new_password2"]) # 加密新密码
blog_user.save() blog_user.save() # 保存用户新密码
return HttpResponseRedirect('/login/') return HttpResponseRedirect('/login/') # 重定向到登录页
else: else:
return self.render_to_response({'form': form}) return self.render_to_response({'form': form}) # 表单验证失败,重新渲染表单
class ForgetPasswordEmailCode(View): class ForgetPasswordEmailCode(View):
def post(self, request: HttpRequest): def post(self, request: HttpRequest):
form = ForgetPasswordCodeForm(request.POST) form = ForgetPasswordCodeForm(request.POST) # 创建忘记密码验证码表单
if not form.is_valid(): if not form.is_valid(): # 表单验证
return HttpResponse("错误的邮箱") return HttpResponse("错误的邮箱") # 邮箱格式错误
to_email = form.cleaned_data["email"] to_email = form.cleaned_data["email"] # 获取邮箱地址
code = generate_code() code = generate_code() # 生成验证码
utils.send_verify_email(to_email, code) utils.send_verify_email(to_email, code) # 发送验证邮件
utils.set_code(to_email, code) utils.set_code(to_email, code) # 保存验证码到缓存
return HttpResponse("ok") return HttpResponse("ok") # 返回成功响应

@ -10,105 +10,113 @@ from .models import Article, Category, Tag, Links, SideBar, BlogSettings
class ArticleForm(forms.ModelForm): class ArticleForm(forms.ModelForm):
# body = forms.CharField(widget=AdminPagedownWidget()) # body = forms.CharField(widget=AdminPagedownWidget()) # 注释掉的Markdown编辑器部件
class Meta: class Meta:
model = Article model = Article # 指定模型为Article
fields = '__all__' fields = '__all__' # 包含所有字段
def makr_article_publish(modeladmin, request, queryset): def makr_article_publish(modeladmin, request, queryset):
queryset.update(status='p') # 发布选中文章的动作
queryset.update(status='p') # 将状态设置为已发布
def draft_article(modeladmin, request, queryset): def draft_article(modeladmin, request, queryset):
queryset.update(status='d') # 将选中文章设为草稿的动作
queryset.update(status='d') # 将状态设置为草稿
def close_article_commentstatus(modeladmin, request, queryset): def close_article_commentstatus(modeladmin, request, queryset):
queryset.update(comment_status='c') # 关闭选中文章评论的动作
queryset.update(comment_status='c') # 将评论状态设置为关闭
def open_article_commentstatus(modeladmin, request, queryset): 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') makr_article_publish.short_description = _('Publish selected articles') # 动作显示名称
draft_article.short_description = _('Draft selected articles') draft_article.short_description = _('Draft selected articles') # 动作显示名称
close_article_commentstatus.short_description = _('Close article comments') close_article_commentstatus.short_description = _('Close article comments') # 动作显示名称
open_article_commentstatus.short_description = _('Open article comments') open_article_commentstatus.short_description = _('Open article comments') # 动作显示名称
class ArticlelAdmin(admin.ModelAdmin): class ArticlelAdmin(admin.ModelAdmin):
list_per_page = 20 list_per_page = 20 # 每页显示20条记录
search_fields = ('body', 'title') search_fields = ('body', 'title') # 搜索字段
form = ArticleForm form = ArticleForm # 使用自定义表单
list_display = ( list_display = (
'id', 'id',
'title', 'title',
'author', 'author',
'link_to_category', 'link_to_category', # 自定义链接字段
'creation_time', 'creation_time',
'views', 'views',
'status', 'status',
'type', 'type',
'article_order') 'article_order') # 列表页显示的字段
list_display_links = ('id', 'title') list_display_links = ('id', 'title') # 可点击链接的字段
list_filter = ('status', 'type', 'category') list_filter = ('status', 'type', 'category') # 右侧过滤器
date_hierarchy = 'creation_time' date_hierarchy = 'creation_time' # 日期层级导航
filter_horizontal = ('tags',) filter_horizontal = ('tags',) # 水平多对多选择器
exclude = ('creation_time', 'last_modify_time') exclude = ('creation_time', 'last_modify_time') # 排除的字段
view_on_site = True view_on_site = True # 启用"在站点查看"功能
actions = [ actions = [
makr_article_publish, makr_article_publish,
draft_article, draft_article,
close_article_commentstatus, close_article_commentstatus,
open_article_commentstatus] open_article_commentstatus] # 管理员动作列表
raw_id_fields = ('author', 'category',) raw_id_fields = ('author', 'category',) # 使用原始ID字段外键搜索
def link_to_category(self, obj): 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,)) info = (obj.category._meta.app_label, obj.category._meta.model_name) # 获取分类模型信息
return format_html(u'<a href="%s">%s</a>' % (link, obj.category.name)) link = reverse('admin:%s_%s_change' % info, args=(obj.category.id,)) # 生成分类编辑链接
return format_html(u'<a href="%s">%s</a>' % (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): 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( form.base_fields['author'].queryset = get_user_model(
).objects.filter(is_superuser=True) ).objects.filter(is_superuser=True) # 限制作者只能选择超级用户
return form return form
def save_model(self, request, obj, form, change): 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): def get_view_on_site_url(self, obj=None):
# 获取"在站点查看"链接的方法
if obj: if obj:
url = obj.get_full_url() url = obj.get_full_url() # 获取文章完整URL
return url return url
else: else:
from djangoblog.utils import get_current_site from djangoblog.utils import get_current_site
site = get_current_site().domain site = get_current_site().domain # 获取当前站点域名
return site return site
class TagAdmin(admin.ModelAdmin): class TagAdmin(admin.ModelAdmin):
exclude = ('slug', 'last_mod_time', 'creation_time') exclude = ('slug', 'last_modify_time', 'creation_time') # 排除自动生成的字段
class CategoryAdmin(admin.ModelAdmin): class CategoryAdmin(admin.ModelAdmin):
list_display = ('name', 'parent_category', 'index') list_display = ('name', 'parent_category', 'index') # 列表页显示字段
exclude = ('slug', 'last_mod_time', 'creation_time') exclude = ('slug', 'last_modify_time', 'creation_time') # 排除自动生成的字段
class LinksAdmin(admin.ModelAdmin): class LinksAdmin(admin.ModelAdmin):
exclude = ('last_mod_time', 'creation_time') exclude = ('last_modify_time', 'creation_time') # 排除时间字段
class SideBarAdmin(admin.ModelAdmin): class SideBarAdmin(admin.ModelAdmin):
list_display = ('name', 'content', 'is_enable', 'sequence') list_display = ('name', 'content', 'is_enable', 'sequence') # 列表页显示字段
exclude = ('last_mod_time', 'creation_time') exclude = ('last_modify_time', 'creation_time') # 排除时间字段
class BlogSettingsAdmin(admin.ModelAdmin): class BlogSettingsAdmin(admin.ModelAdmin):
pass pass # 使用默认的ModelAdmin配置

@ -2,4 +2,4 @@ from django.apps import AppConfig
class BlogConfig(AppConfig): class BlogConfig(AppConfig):
name = 'blog' name = 'blog' # 指定应用的Python路径为'blog'

@ -9,35 +9,36 @@ logger = logging.getLogger(__name__)
def seo_processor(requests): def seo_processor(requests):
key = 'seo_processor' # SEO上下文处理器为模板提供SEO相关变量
value = cache.get(key) key = 'seo_processor' # 缓存键
value = cache.get(key) # 尝试从缓存获取数据
if value: if value:
return value return value # 如果缓存存在,直接返回
else: else:
logger.info('set processor cache.') logger.info('set processor cache.') # 记录缓存设置日志
setting = get_blog_setting() setting = get_blog_setting() # 获取博客设置
value = { value = {
'SITE_NAME': setting.site_name, 'SITE_NAME': setting.site_name, # 网站名称
'SHOW_GOOGLE_ADSENSE': setting.show_google_adsense, 'SHOW_GOOGLE_ADSENSE': setting.show_google_adsense, # 是否显示Google广告
'GOOGLE_ADSENSE_CODES': setting.google_adsense_codes, 'GOOGLE_ADSENSE_CODES': setting.google_adsense_codes, # Google广告代码
'SITE_SEO_DESCRIPTION': setting.site_seo_description, 'SITE_SEO_DESCRIPTION': setting.site_seo_description, # 网站SEO描述
'SITE_DESCRIPTION': setting.site_description, 'SITE_DESCRIPTION': setting.site_description, # 网站描述
'SITE_KEYWORDS': setting.site_keywords, 'SITE_KEYWORDS': setting.site_keywords, # 网站关键词
'SITE_BASE_URL': requests.scheme + '://' + requests.get_host() + '/', 'SITE_BASE_URL': requests.scheme + '://' + requests.get_host() + '/', # 网站基础URL
'ARTICLE_SUB_LENGTH': setting.article_sub_length, 'ARTICLE_SUB_LENGTH': setting.article_sub_length, # 文章摘要长度
'nav_category_list': Category.objects.all(), 'nav_category_list': Category.objects.all(), # 导航分类列表
'nav_pages': Article.objects.filter( 'nav_pages': Article.objects.filter(
type='p', type='p', # 页面类型
status='p'), status='p'), # 已发布状态
'OPEN_SITE_COMMENT': setting.open_site_comment, 'OPEN_SITE_COMMENT': setting.open_site_comment, # 是否开启网站评论
'BEIAN_CODE': setting.beian_code, 'BEIAN_CODE': setting.beian_code, # 备案号
'ANALYTICS_CODE': setting.analytics_code, 'ANALYTICS_CODE': setting.analytics_code, # 网站统计代码
"BEIAN_CODE_GONGAN": setting.gongan_beiancode, "BEIAN_CODE_GONGAN": setting.gongan_beiancode, # 公安备案号
"SHOW_GONGAN_CODE": setting.show_gongan_code, "SHOW_GONGAN_CODE": setting.show_gongan_code, # 是否显示公安备案
"CURRENT_YEAR": timezone.now().year, "CURRENT_YEAR": timezone.now().year, # 当前年份
"GLOBAL_HEADER": setting.global_header, "GLOBAL_HEADER": setting.global_header, # 全局头部内容
"GLOBAL_FOOTER": setting.global_footer, "GLOBAL_FOOTER": setting.global_footer, # 全局尾部内容
"COMMENT_NEED_REVIEW": setting.comment_need_review, "COMMENT_NEED_REVIEW": setting.comment_need_review, # 评论是否需要审核
} }
cache.set(key, value, 60 * 60 * 10) cache.set(key, value, 60 * 60 * 10) # 设置缓存有效期10小时
return value return value # 返回上下文数据

@ -1,5 +1,3 @@
import time
import elasticsearch.client import elasticsearch.client
from django.conf import settings from django.conf import settings
from elasticsearch_dsl import Document, InnerDoc, Date, Integer, Long, Text, Object, GeoPoint, Keyword, Boolean 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 from blog.models import Article
ELASTICSEARCH_ENABLED = hasattr(settings, 'ELASTICSEARCH_DSL') ELASTICSEARCH_ENABLED = hasattr(settings, 'ELASTICSEARCH_DSL') # 检查是否启用Elasticsearch
if ELASTICSEARCH_ENABLED: if ELASTICSEARCH_ENABLED:
connections.create_connection( connections.create_connection(
hosts=[settings.ELASTICSEARCH_DSL['default']['hosts']]) hosts=[settings.ELASTICSEARCH_DSL['default']['hosts']]) # 创建Elasticsearch连接
from elasticsearch import 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 from elasticsearch.client import IngestClient
c = IngestClient(es) c = IngestClient(es) # Ingest管道客户端
try: try:
c.get_pipeline('geoip') c.get_pipeline('geoip') # 尝试获取geoip管道
except elasticsearch.exceptions.NotFoundError: except elasticsearch.exceptions.NotFoundError:
c.put_pipeline('geoip', body='''{ c.put_pipeline('geoip', body='''{
"description" : "Add geoip info", "description" : "Add geoip info",
@ -30,158 +28,172 @@ if ELASTICSEARCH_ENABLED:
} }
} }
] ]
}''') }''') # 创建geoip处理管道
class GeoIp(InnerDoc): class GeoIp(InnerDoc):
continent_name = Keyword() # GeoIP地理位置信息内嵌文档
country_iso_code = Keyword() continent_name = Keyword() # 大洲名称
country_name = Keyword() country_iso_code = Keyword() # 国家ISO代码
location = GeoPoint() country_name = Keyword() # 国家名称
location = GeoPoint() # 地理位置坐标
class UserAgentBrowser(InnerDoc): class UserAgentBrowser(InnerDoc):
Family = Keyword() # 用户代理浏览器信息
Version = Keyword() Family = Keyword() # 浏览器家族
Version = Keyword() # 浏览器版本
class UserAgentOS(UserAgentBrowser): class UserAgentOS(UserAgentBrowser):
# 用户代理操作系统信息
pass pass
class UserAgentDevice(InnerDoc): class UserAgentDevice(InnerDoc):
Family = Keyword() # 用户代理设备信息
Brand = Keyword() Family = Keyword() # 设备家族
Model = Keyword() Brand = Keyword() # 设备品牌
Model = Keyword() # 设备型号
class UserAgent(InnerDoc): class UserAgent(InnerDoc):
browser = Object(UserAgentBrowser, required=False) # 完整的用户代理信息
os = Object(UserAgentOS, required=False) browser = Object(UserAgentBrowser, required=False) # 浏览器对象
device = Object(UserAgentDevice, required=False) os = Object(UserAgentOS, required=False) # 操作系统对象
string = Text() device = Object(UserAgentDevice, required=False) # 设备对象
is_bot = Boolean() string = Text() # 原始用户代理字符串
is_bot = Boolean() # 是否为机器人
class ElapsedTimeDocument(Document): class ElapsedTimeDocument(Document):
url = Keyword() # 响应时间文档,用于性能监控
time_taken = Long() url = Keyword() # 请求URL
log_datetime = Date() time_taken = Long() # 耗时(毫秒)
ip = Keyword() log_datetime = Date() # 日志时间
geoip = Object(GeoIp, required=False) ip = Keyword() # IP地址
useragent = Object(UserAgent, required=False) geoip = Object(GeoIp, required=False) # GeoIP地理位置信息
useragent = Object(UserAgent, required=False) # 用户代理信息
class Index: class Index:
name = 'performance' name = 'performance' # 索引名称
settings = { settings = {
"number_of_shards": 1, "number_of_shards": 1, # 分片数量
"number_of_replicas": 0 "number_of_replicas": 0 # 副本数量
} }
class Meta: class Meta:
doc_type = 'ElapsedTime' doc_type = 'ElapsedTime' # 文档类型
class ElaspedTimeDocumentManager: class ElaspedTimeDocumentManager:
# 响应时间文档管理器
@staticmethod @staticmethod
def build_index(): def build_index():
# 构建索引
from elasticsearch import Elasticsearch from elasticsearch import Elasticsearch
client = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts']) client = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
res = client.indices.exists(index="performance") res = client.indices.exists(index="performance") # 检查索引是否存在
if not res: if not res:
ElapsedTimeDocument.init() ElapsedTimeDocument.init() # 初始化索引
@staticmethod @staticmethod
def delete_index(): def delete_index():
# 删除索引
from elasticsearch import Elasticsearch from elasticsearch import Elasticsearch
es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts']) 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 @staticmethod
def create(url, time_taken, log_datetime, useragent, ip): def create(url, time_taken, log_datetime, useragent, ip):
ElaspedTimeDocumentManager.build_index() # 创建响应时间文档记录
ElaspedTimeDocumentManager.build_index() # 确保索引存在
ua = UserAgent() ua = UserAgent()
ua.browser = UserAgentBrowser() ua.browser = UserAgentBrowser()
ua.browser.Family = useragent.browser.family ua.browser.Family = useragent.browser.family # 浏览器家族
ua.browser.Version = useragent.browser.version_string ua.browser.Version = useragent.browser.version_string # 浏览器版本
ua.os = UserAgentOS() ua.os = UserAgentOS()
ua.os.Family = useragent.os.family ua.os.Family = useragent.os.family # 操作系统家族
ua.os.Version = useragent.os.version_string ua.os.Version = useragent.os.version_string # 操作系统版本
ua.device = UserAgentDevice() ua.device = UserAgentDevice()
ua.device.Family = useragent.device.family ua.device.Family = useragent.device.family # 设备家族
ua.device.Brand = useragent.device.brand ua.device.Brand = useragent.device.brand # 设备品牌
ua.device.Model = useragent.device.model ua.device.Model = useragent.device.model # 设备型号
ua.string = useragent.ua_string ua.string = useragent.ua_string # 原始用户代理字符串
ua.is_bot = useragent.is_bot ua.is_bot = useragent.is_bot # 是否为机器人
doc = ElapsedTimeDocument( doc = ElapsedTimeDocument(
meta={ meta={
'id': int( 'id': int(
round( round(
time.time() * time.time() *
1000)) 1000)) # 使用时间戳作为文档ID
}, },
url=url, url=url,
time_taken=time_taken, time_taken=time_taken,
log_datetime=log_datetime, log_datetime=log_datetime,
useragent=ua, ip=ip) useragent=ua, ip=ip)
doc.save(pipeline="geoip") doc.save(pipeline="geoip") # 保存文档并使用geoip管道处理
class ArticleDocument(Document): 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={ author = Object(properties={
'nickname': Text(analyzer='ik_max_word', search_analyzer='ik_smart'), 'nickname': Text(analyzer='ik_max_word', search_analyzer='ik_smart'), # 作者昵称
'id': Integer() 'id': Integer() # 作者ID
}) })
category = Object(properties={ category = Object(properties={
'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'), 'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'), # 分类名称
'id': Integer() 'id': Integer() # 分类ID
}) })
tags = Object(properties={ tags = Object(properties={
'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'), 'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'), # 标签名称
'id': Integer() 'id': Integer() # 标签ID
}) })
pub_time = Date() pub_time = Date() # 发布时间
status = Text() status = Text() # 文章状态
comment_status = Text() comment_status = Text() # 评论状态
type = Text() type = Text() # 文章类型
views = Integer() views = Integer() # 浏览量
article_order = Integer() article_order = Integer() # 文章排序
class Index: class Index:
name = 'blog' name = 'blog' # 索引名称
settings = { settings = {
"number_of_shards": 1, "number_of_shards": 1, # 分片数量
"number_of_replicas": 0 "number_of_replicas": 0 # 副本数量
} }
class Meta: class Meta:
doc_type = 'Article' doc_type = 'Article' # 文档类型
class ArticleDocumentManager(): class ArticleDocumentManager():
# 文章文档管理器
def __init__(self): def __init__(self):
self.create_index() self.create_index() # 初始化时创建索引
def create_index(self): def create_index(self):
ArticleDocument.init() ArticleDocument.init() # 初始化文章索引
def delete_index(self): def delete_index(self):
# 删除文章索引
from elasticsearch import Elasticsearch from elasticsearch import Elasticsearch
es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts']) 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): def convert_to_doc(self, articles):
# 将文章模型转换为Elasticsearch文档
return [ return [
ArticleDocument( ArticleDocument(
meta={ meta={
'id': article.id}, 'id': article.id}, # 使用文章ID作为文档ID
body=article.body, body=article.body,
title=article.title, title=article.title,
author={ author={
@ -193,7 +205,7 @@ class ArticleDocumentManager():
tags=[ tags=[
{ {
'name': t.name, '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, pub_time=article.pub_time,
status=article.status, status=article.status,
comment_status=article.comment_status, comment_status=article.comment_status,
@ -202,12 +214,14 @@ class ArticleDocumentManager():
article_order=article.article_order) for article in articles] article_order=article.article_order) for article in articles]
def rebuild(self, articles=None): def rebuild(self, articles=None):
ArticleDocument.init() # 重建索引
articles = articles if articles else Article.objects.all() ArticleDocument.init() # 重新初始化索引
docs = self.convert_to_doc(articles) articles = articles if articles else Article.objects.all() # 获取所有文章或指定文章
docs = self.convert_to_doc(articles) # 转换为文档
for doc in docs: for doc in docs:
doc.save() doc.save() # 保存所有文档
def update_docs(self, docs): def update_docs(self, docs):
# 更新文档
for doc in docs: for doc in docs:
doc.save() doc.save() # 保存更新的文档

@ -7,13 +7,14 @@ logger = logging.getLogger(__name__)
class BlogSearchForm(SearchForm): class BlogSearchForm(SearchForm):
querydata = forms.CharField(required=True) querydata = forms.CharField(required=True) # 搜索查询字段,必须填写
def search(self): def search(self):
datas = super(BlogSearchForm, self).search() # 重写搜索方法
if not self.is_valid(): datas = super(BlogSearchForm, self).search() # 调用父类的搜索方法
return self.no_query_found() if not self.is_valid(): # 检查表单是否有效
return self.no_query_found() # 返回无查询结果
if self.cleaned_data['querydata']: if self.cleaned_data['querydata']:
logger.info(self.cleaned_data['querydata']) logger.info(self.cleaned_data['querydata']) # 记录搜索查询日志
return datas return datas # 返回搜索结果

@ -6,13 +6,14 @@ from blog.documents import ElapsedTimeDocument, ArticleDocumentManager, ElaspedT
# TODO 参数化 # TODO 参数化
class Command(BaseCommand): class Command(BaseCommand):
help = 'build search index' help = 'build search index' # 命令帮助信息
def handle(self, *args, **options): def handle(self, *args, **options):
if ELASTICSEARCH_ENABLED: # 处理命令的主方法
ElaspedTimeDocumentManager.build_index() if ELASTICSEARCH_ENABLED: # 检查Elasticsearch是否启用
manager = ElapsedTimeDocument() ElaspedTimeDocumentManager.build_index() # 构建耗时文档索引
manager.init() manager = ElapsedTimeDocument() # 创建耗时文档管理器实例
manager = ArticleDocumentManager() manager.init() # 初始化耗时文档
manager.delete_index() manager = ArticleDocumentManager() # 创建文章文档管理器实例
manager.rebuild() manager.delete_index() # 删除现有文章索引
manager.rebuild() # 重新构建文章索引

@ -5,9 +5,12 @@ from blog.models import Tag, Category
# TODO 参数化 # TODO 参数化
class Command(BaseCommand): class Command(BaseCommand):
help = 'build search words' help = 'build search words' # 命令帮助信息:构建搜索词
def handle(self, *args, **options): def handle(self, *args, **options):
# 处理命令的主方法
# 从标签和分类中获取所有名称,使用集合去重
datas = set([t.name for t in Tag.objects.all()] + datas = set([t.name for t in Tag.objects.all()] +
[t.name for t in Category.objects.all()]) [t.name for t in Category.objects.all()])
print('\n'.join(datas)) # 将去重后的数据按行打印输出
print('\n'.join(datas))

@ -4,8 +4,9 @@ from djangoblog.utils import cache
class Command(BaseCommand): class Command(BaseCommand):
help = 'clear the whole cache' help = 'clear the whole cache' # 命令帮助信息:清除整个缓存
def handle(self, *args, **options): 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')) # 输出成功信息

@ -6,35 +6,44 @@ from blog.models import Article, Tag, Category
class Command(BaseCommand): class Command(BaseCommand):
help = 'create test datas' help = 'create test datas' # 命令帮助信息:创建测试数据
def handle(self, *args, **options): def handle(self, *args, **options):
# 处理命令的主方法
# 获取或创建测试用户
user = get_user_model().objects.get_or_create( user = get_user_model().objects.get_or_create(
email='test@test.com', username='测试用户', password=make_password('test!q@w#eTYU'))[0] email='test@test.com', username='测试用户', password=make_password('test!q@w#eTYU'))[0]
# 获取或创建父类目
pcategory = Category.objects.get_or_create( pcategory = Category.objects.get_or_create(
name='我是父类目', parent_category=None)[0] name='我是父类目', parent_category=None)[0]
# 获取或创建子类目
category = Category.objects.get_or_create( category = Category.objects.get_or_create(
name='子类目', parent_category=pcategory)[0] name='子类目', parent_category=pcategory)[0]
category.save() category.save() # 保存子类目
# 创建基础标签
basetag = Tag() basetag = Tag()
basetag.name = "标签" basetag.name = "标签"
basetag.save() basetag.save()
# 循环创建测试文章
for i in range(1, 20): for i in range(1, 20):
# 获取或创建文章
article = Article.objects.get_or_create( article = Article.objects.get_or_create(
category=category, category=category,
title='nice title ' + str(i), title='nice title ' + str(i),
body='nice content ' + str(i), body='nice content ' + str(i),
author=user)[0] author=user)[0]
# 创建文章专属标签
tag = Tag() tag = Tag()
tag.name = "标签" + str(i) tag.name = "标签" + str(i)
tag.save() tag.save()
# 为文章添加标签
article.tags.add(tag) article.tags.add(tag)
article.tags.add(basetag) article.tags.add(basetag)
article.save() article.save() # 保存文章
from djangoblog.utils import cache from djangoblog.utils import cache
cache.clear() cache.clear() # 清除缓存
self.stdout.write(self.style.SUCCESS('created test datas \n')) self.stdout.write(self.style.SUCCESS('created test datas \n')) # 输出成功信息

@ -4,47 +4,52 @@ from djangoblog.spider_notify import SpiderNotify
from djangoblog.utils import get_current_site from djangoblog.utils import get_current_site
from blog.models import Article, Tag, Category from blog.models import Article, Tag, Category
site = get_current_site().domain site = get_current_site().domain # 获取当前站点域名
class Command(BaseCommand): class Command(BaseCommand):
help = 'notify baidu url' help = 'notify baidu url' # 命令帮助信息通知百度URL
def add_arguments(self, parser): def add_arguments(self, parser):
# 添加命令行参数
parser.add_argument( parser.add_argument(
'data_type', 'data_type', # 参数名
type=str, type=str, # 参数类型
choices=[ choices=[
'all', 'all',
'article', 'article',
'tag', 'tag',
'category'], 'category'], # 参数可选值
help='article : all article,tag : all tag,category: all category,all: All of these') help='article : all article,tag : all tag,category: all category,all: All of these') # 参数帮助信息
def get_full_url(self, path): def get_full_url(self, path):
# 根据相对路径构建完整URL
url = "https://{site}{path}".format(site=site, path=path) url = "https://{site}{path}".format(site=site, path=path)
return url return url
def handle(self, *args, **options): def handle(self, *args, **options):
type = options['data_type'] type = options['data_type'] # 获取数据类型参数
self.stdout.write('start get %s' % type) self.stdout.write('start get %s' % type) # 输出开始信息
urls = [] urls = [] # 初始化URL列表
# 根据类型收集文章URL
if type == 'article' or type == 'all': if type == 'article' or type == 'all':
for article in Article.objects.filter(status='p'): for article in Article.objects.filter(status='p'): # 只获取已发布的文章
urls.append(article.get_full_url()) urls.append(article.get_full_url()) # 添加文章完整URL
# 根据类型收集标签URL
if type == 'tag' or type == 'all': if type == 'tag' or type == 'all':
for tag in Tag.objects.all(): for tag in Tag.objects.all():
url = tag.get_absolute_url() url = tag.get_absolute_url() # 获取标签相对URL
urls.append(self.get_full_url(url)) urls.append(self.get_full_url(url)) # 添加标签完整URL
# 根据类型收集分类URL
if type == 'category' or type == 'all': if type == 'category' or type == 'all':
for category in Category.objects.all(): for category in Category.objects.all():
url = category.get_absolute_url() url = category.get_absolute_url() # 获取分类相对URL
urls.append(self.get_full_url(url)) urls.append(self.get_full_url(url)) # 添加分类完整URL
self.stdout.write( self.stdout.write(
self.style.SUCCESS( self.style.SUCCESS(
'start notify %d urls' % 'start notify %d urls' %
len(urls))) len(urls))) # 输出开始通知信息
SpiderNotify.baidu_notify(urls) SpiderNotify.baidu_notify(urls) # 调用百度站长平台URL推送
self.stdout.write(self.style.SUCCESS('finish notify')) self.stdout.write(self.style.SUCCESS('finish notify')) # 输出完成信息

@ -8,40 +8,41 @@ from oauth.oauthmanager import get_manager_by_type
class Command(BaseCommand): class Command(BaseCommand):
help = 'sync user avatar' help = 'sync user avatar' # 命令帮助信息:同步用户头像
def test_picture(self, url): def test_picture(self, url):
# 测试图片URL是否可访问
try: try:
if requests.get(url, timeout=2).status_code == 200: if requests.get(url, timeout=2).status_code == 200: # 发送HTTP请求测试图片
return True return True # 图片可访问返回True
except: except:
pass pass # 发生异常时静默处理
def handle(self, *args, **options): def handle(self, *args, **options):
static_url = static("../") static_url = static("../") # 获取静态文件基础URL
users = OAuthUser.objects.all() users = OAuthUser.objects.all() # 获取所有OAuth用户
self.stdout.write(f'开始同步{len(users)}个用户头像') self.stdout.write(f'开始同步{len(users)}个用户头像') # 输出开始同步信息
for u in users: for u in users:
self.stdout.write(f'开始同步:{u.nickname}') self.stdout.write(f'开始同步:{u.nickname}') # 输出当前同步的用户
url = u.picture url = u.picture # 获取用户当前头像URL
if url: if url:
if url.startswith(static_url): if url.startswith(static_url): # 检查是否已经是静态文件URL
if self.test_picture(url): if self.test_picture(url): # 测试静态图片是否可访问
continue continue # 可访问则跳过处理
else: else:
if u.metadata: if u.metadata: # 如果有用户元数据
manage = get_manager_by_type(u.type) manage = get_manager_by_type(u.type) # 根据OAuth类型获取管理器
url = manage.get_picture(u.metadata) url = manage.get_picture(u.metadata) # 从OAuth平台获取头像URL
url = save_user_avatar(url) url = save_user_avatar(url) # 保存头像并返回新URL
else: else:
url = static('blog/img/avatar.png') url = static('blog/img/avatar.png') # 使用默认头像
else: else:
url = save_user_avatar(url) url = save_user_avatar(url) # 保存外部头像并返回新URL
else: else:
url = static('blog/img/avatar.png') url = static('blog/img/avatar.png') # 没有头像时使用默认头像
if url: if url:
self.stdout.write( self.stdout.write(
f'结束同步:{u.nickname}.url:{url}') f'结束同步:{u.nickname}.url:{url}') # 输出同步完成信息
u.picture = url u.picture = url # 更新用户头像URL
u.save() u.save() # 保存用户信息
self.stdout.write('结束同步') self.stdout.write('结束同步') # 输出同步结束信息

@ -11,32 +11,32 @@ logger = logging.getLogger(__name__)
class OnlineMiddleware(object): class OnlineMiddleware(object):
def __init__(self, get_response=None): def __init__(self, get_response=None):
self.get_response = get_response self.get_response = get_response # 获取响应的方法
super().__init__() super().__init__()
def __call__(self, request): def __call__(self, request):
''' page render time ''' ''' page render time ''' # 页面渲染时间统计中间件
start_time = time.time() start_time = time.time() # 记录开始时间
response = self.get_response(request) response = self.get_response(request) # 获取响应
http_user_agent = request.META.get('HTTP_USER_AGENT', '') http_user_agent = request.META.get('HTTP_USER_AGENT', '') # 获取用户代理
ip, _ = get_client_ip(request) ip, _ = get_client_ip(request) # 获取客户端IP
user_agent = parse(http_user_agent) user_agent = parse(http_user_agent) # 解析用户代理
if not response.streaming: if not response.streaming: # 如果不是流式响应
try: try:
cast_time = time.time() - start_time cast_time = time.time() - start_time # 计算耗时
if ELASTICSEARCH_ENABLED: if ELASTICSEARCH_ENABLED: # 如果启用了Elasticsearch
time_taken = round((cast_time) * 1000, 2) time_taken = round((cast_time) * 1000, 2) # 转换为毫秒并保留两位小数
url = request.path url = request.path # 获取请求路径
from django.utils import timezone from django.utils import timezone
ElaspedTimeDocumentManager.create( ElaspedTimeDocumentManager.create(
url=url, url=url,
time_taken=time_taken, time_taken=time_taken,
log_datetime=timezone.now(), log_datetime=timezone.now(), # 当前时间
useragent=user_agent, useragent=user_agent,
ip=ip) ip=ip) # 创建性能监控记录
response.content = response.content.replace( response.content = response.content.replace(
b'<!!LOAD_TIMES!!>', str.encode(str(cast_time)[:5])) b'<!!LOAD_TIMES!!>', str.encode(str(cast_time)[:5])) # 替换加载时间占位符
except Exception as e: except Exception as e:
logger.error("Error OnlineMiddleware: %s" % e) logger.error("Error OnlineMiddleware: %s" % e) # 记录错误日志
return response return response # 返回响应

@ -9,13 +9,14 @@ import mdeditor.fields
class Migration(migrations.Migration): class Migration(migrations.Migration):
initial = True initial = True # 标记为初始迁移
dependencies = [ dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL), migrations.swappable_dependency(settings.AUTH_USER_MODEL), # 依赖用户模型
] ]
operations = [ operations = [
# 创建网站配置表
migrations.CreateModel( migrations.CreateModel(
name='BlogSettings', name='BlogSettings',
fields=[ fields=[
@ -37,10 +38,11 @@ class Migration(migrations.Migration):
('gongan_beiancode', models.TextField(blank=True, default='', max_length=2000, null=True, verbose_name='公安备案号')), ('gongan_beiancode', models.TextField(blank=True, default='', max_length=2000, null=True, verbose_name='公安备案号')),
], ],
options={ options={
'verbose_name': '网站配置', 'verbose_name': '网站配置', # 单数显示名称
'verbose_name_plural': '网站配置', 'verbose_name_plural': '网站配置', # 复数显示名称
}, },
), ),
# 创建友情链接表
migrations.CreateModel( migrations.CreateModel(
name='Links', name='Links',
fields=[ fields=[
@ -56,9 +58,10 @@ class Migration(migrations.Migration):
options={ options={
'verbose_name': '友情链接', 'verbose_name': '友情链接',
'verbose_name_plural': '友情链接', 'verbose_name_plural': '友情链接',
'ordering': ['sequence'], 'ordering': ['sequence'], # 按排序字段升序排列
}, },
), ),
# 创建侧边栏表
migrations.CreateModel( migrations.CreateModel(
name='SideBar', name='SideBar',
fields=[ fields=[
@ -73,49 +76,52 @@ class Migration(migrations.Migration):
options={ options={
'verbose_name': '侧边栏', 'verbose_name': '侧边栏',
'verbose_name_plural': '侧边栏', 'verbose_name_plural': '侧边栏',
'ordering': ['sequence'], 'ordering': ['sequence'], # 按排序字段升序排列
}, },
), ),
# 创建标签表
migrations.CreateModel( migrations.CreateModel(
name='Tag', name='Tag',
fields=[ 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='创建时间')), ('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
('last_mod_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='标签名')), ('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={ options={
'verbose_name': '标签', 'verbose_name': '标签',
'verbose_name_plural': '标签', 'verbose_name_plural': '标签',
'ordering': ['name'], 'ordering': ['name'], # 按名称升序排列
}, },
), ),
# 创建分类表
migrations.CreateModel( migrations.CreateModel(
name='Category', name='Category',
fields=[ 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='创建时间')), ('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
('last_mod_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='分类名')), ('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='权重排序-越大越靠前')), ('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={ options={
'verbose_name': '分类', 'verbose_name': '分类',
'verbose_name_plural': '分类', 'verbose_name_plural': '分类',
'ordering': ['-index'], 'ordering': ['-index'], # 按权重倒序排列
}, },
), ),
# 创建文章表
migrations.CreateModel( migrations.CreateModel(
name='Article', name='Article',
fields=[ 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='创建时间')), ('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
('last_mod_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='标题')), ('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='发布时间')), ('pub_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='发布时间')),
('status', models.CharField(choices=[('d', '草稿'), ('p', '发表')], default='p', max_length=1, 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='评论状态')), ('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='浏览量')), ('views', models.PositiveIntegerField(default=0, verbose_name='浏览量')),
('article_order', models.IntegerField(default=0, verbose_name='排序,数字越大越靠前')), ('article_order', models.IntegerField(default=0, verbose_name='排序,数字越大越靠前')),
('show_toc', models.BooleanField(default=False, verbose_name='是否显示toc目录')), ('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='作者')), ('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='分类')), ('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='标签集合')), ('tags', models.ManyToManyField(blank=True, to='blog.tag', verbose_name='标签集合')), # 多对多关联标签
], ],
options={ options={
'verbose_name': '文章', 'verbose_name': '文章',
'verbose_name_plural': '文章', 'verbose_name_plural': '文章',
'ordering': ['-article_order', '-pub_time'], 'ordering': ['-article_order', '-pub_time'], # 按文章排序和发布时间倒序排列
'get_latest_by': 'id', 'get_latest_by': 'id', # 指定获取最新记录的字段
}, },
), ),
] ]

@ -6,18 +6,20 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('blog', '0001_initial'), ('blog', '0001_initial'), # 依赖blog应用的初始迁移
] ]
operations = [ operations = [
# 向BlogSettings模型添加global_footer字段
migrations.AddField( migrations.AddField(
model_name='blogsettings', model_name='blogsettings',
name='global_footer', name='global_footer',
field=models.TextField(blank=True, default='', null=True, verbose_name='公共尾部'), field=models.TextField(blank=True, default='', null=True, verbose_name='公共尾部'),
), ),
# 向BlogSettings模型添加global_header字段
migrations.AddField( migrations.AddField(
model_name='blogsettings', model_name='blogsettings',
name='global_header', name='global_header',
field=models.TextField(blank=True, default='', null=True, verbose_name='公共头部'), field=models.TextField(blank=True, default='', null=True, verbose_name='公共头部'),
), ),
] ]

@ -5,13 +5,14 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('blog', '0002_blogsettings_global_footer_and_more'), ('blog', '0002_blogsettings_global_footer_and_more'), # 依赖blog应用的第二个迁移
] ]
operations = [ operations = [
# 向BlogSettings模型添加comment_need_review字段
migrations.AddField( migrations.AddField(
model_name='blogsettings', model_name='blogsettings',
name='comment_need_review', name='comment_need_review',
field=models.BooleanField(default=False, verbose_name='评论是否需要审核'), field=models.BooleanField(default=False, verbose_name='评论是否需要审核'),
), ),
] ]

@ -5,23 +5,26 @@ from django.db import migrations
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('blog', '0003_blogsettings_comment_need_review'), ('blog', '0003_blogsettings_comment_need_review'), # 依赖blog应用的第三个迁移
] ]
operations = [ operations = [
# 重命名字段analyticscode -> analytics_code
migrations.RenameField( migrations.RenameField(
model_name='blogsettings', model_name='blogsettings',
old_name='analyticscode', old_name='analyticscode',
new_name='analytics_code', new_name='analytics_code',
), ),
# 重命名字段beiancode -> beian_code
migrations.RenameField( migrations.RenameField(
model_name='blogsettings', model_name='blogsettings',
old_name='beiancode', old_name='beiancode',
new_name='beian_code', new_name='beian_code',
), ),
# 重命名字段sitename -> site_name
migrations.RenameField( migrations.RenameField(
model_name='blogsettings', model_name='blogsettings',
old_name='sitename', old_name='sitename',
new_name='site_name', new_name='site_name',
), ),
] ]

@ -11,290 +11,349 @@ class Migration(migrations.Migration):
dependencies = [ dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL), 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 = [ operations = [
# 修改Article模型的元数据选项国际化
migrations.AlterModelOptions( migrations.AlterModelOptions(
name='article', name='article',
options={'get_latest_by': 'id', 'ordering': ['-article_order', '-pub_time'], 'verbose_name': 'article', 'verbose_name_plural': 'article'}, options={'get_latest_by': 'id', 'ordering': ['-article_order', '-pub_time'], 'verbose_name': 'article', 'verbose_name_plural': 'article'},
), ),
# 修改Category模型的元数据选项国际化
migrations.AlterModelOptions( migrations.AlterModelOptions(
name='category', name='category',
options={'ordering': ['-index'], 'verbose_name': 'category', 'verbose_name_plural': 'category'}, options={'ordering': ['-index'], 'verbose_name': 'category', 'verbose_name_plural': 'category'},
), ),
# 修改Links模型的元数据选项国际化
migrations.AlterModelOptions( migrations.AlterModelOptions(
name='links', name='links',
options={'ordering': ['sequence'], 'verbose_name': 'link', 'verbose_name_plural': 'link'}, options={'ordering': ['sequence'], 'verbose_name': 'link', 'verbose_name_plural': 'link'},
), ),
# 修改Sidebar模型的元数据选项国际化
migrations.AlterModelOptions( migrations.AlterModelOptions(
name='sidebar', name='sidebar',
options={'ordering': ['sequence'], 'verbose_name': 'sidebar', 'verbose_name_plural': 'sidebar'}, options={'ordering': ['sequence'], 'verbose_name': 'sidebar', 'verbose_name_plural': 'sidebar'},
), ),
# 修改Tag模型的元数据选项国际化
migrations.AlterModelOptions( migrations.AlterModelOptions(
name='tag', name='tag',
options={'ordering': ['name'], 'verbose_name': 'tag', 'verbose_name_plural': 'tag'}, options={'ordering': ['name'], 'verbose_name': 'tag', 'verbose_name_plural': 'tag'},
), ),
# 删除Article模型的created_time字段
migrations.RemoveField( migrations.RemoveField(
model_name='article', model_name='article',
name='created_time', name='created_time',
), ),
# 删除Article模型的last_mod_time字段
migrations.RemoveField( migrations.RemoveField(
model_name='article', model_name='article',
name='last_mod_time', name='last_mod_time',
), ),
# 删除Category模型的created_time字段
migrations.RemoveField( migrations.RemoveField(
model_name='category', model_name='category',
name='created_time', name='created_time',
), ),
# 删除Category模型的last_mod_time字段
migrations.RemoveField( migrations.RemoveField(
model_name='category', model_name='category',
name='last_mod_time', name='last_mod_time',
), ),
# 删除Links模型的created_time字段
migrations.RemoveField( migrations.RemoveField(
model_name='links', model_name='links',
name='created_time', name='created_time',
), ),
# 删除Sidebar模型的created_time字段
migrations.RemoveField( migrations.RemoveField(
model_name='sidebar', model_name='sidebar',
name='created_time', name='created_time',
), ),
# 删除Tag模型的created_time字段
migrations.RemoveField( migrations.RemoveField(
model_name='tag', model_name='tag',
name='created_time', name='created_time',
), ),
# 删除Tag模型的last_mod_time字段
migrations.RemoveField( migrations.RemoveField(
model_name='tag', model_name='tag',
name='last_mod_time', name='last_mod_time',
), ),
# 向Article模型添加creation_time字段
migrations.AddField( migrations.AddField(
model_name='article', model_name='article',
name='creation_time', name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'), field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
), ),
# 向Article模型添加last_modify_time字段
migrations.AddField( migrations.AddField(
model_name='article', model_name='article',
name='last_modify_time', name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'), field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
), ),
# 向Category模型添加creation_time字段
migrations.AddField( migrations.AddField(
model_name='category', model_name='category',
name='creation_time', name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'), field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
), ),
# 向Category模型添加last_modify_time字段
migrations.AddField( migrations.AddField(
model_name='category', model_name='category',
name='last_modify_time', name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'), field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
), ),
# 向Links模型添加creation_time字段
migrations.AddField( migrations.AddField(
model_name='links', model_name='links',
name='creation_time', name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'), field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
), ),
# 向Sidebar模型添加creation_time字段
migrations.AddField( migrations.AddField(
model_name='sidebar', model_name='sidebar',
name='creation_time', name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'), field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
), ),
# 向Tag模型添加creation_time字段
migrations.AddField( migrations.AddField(
model_name='tag', model_name='tag',
name='creation_time', name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'), field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
), ),
# 向Tag模型添加last_modify_time字段
migrations.AddField( migrations.AddField(
model_name='tag', model_name='tag',
name='last_modify_time', name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'), field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
), ),
# 修改Article模型的article_order字段verbose_name国际化
migrations.AlterField( migrations.AlterField(
model_name='article', model_name='article',
name='article_order', name='article_order',
field=models.IntegerField(default=0, verbose_name='order'), field=models.IntegerField(default=0, verbose_name='order'),
), ),
# 修改Article模型的author字段verbose_name国际化
migrations.AlterField( migrations.AlterField(
model_name='article', model_name='article',
name='author', name='author',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='author'), field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='author'),
), ),
# 修改Article模型的body字段verbose_name国际化
migrations.AlterField( migrations.AlterField(
model_name='article', model_name='article',
name='body', name='body',
field=mdeditor.fields.MDTextField(verbose_name='body'), field=mdeditor.fields.MDTextField(verbose_name='body'),
), ),
# 修改Article模型的category字段verbose_name国际化
migrations.AlterField( migrations.AlterField(
model_name='article', model_name='article',
name='category', name='category',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_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( migrations.AlterField(
model_name='article', model_name='article',
name='comment_status', name='comment_status',
field=models.CharField(choices=[('o', 'Open'), ('c', 'Close')], default='o', max_length=1, verbose_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( migrations.AlterField(
model_name='article', model_name='article',
name='pub_time', name='pub_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='publish time'), field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='publish time'),
), ),
# 修改Article模型的show_toc字段verbose_name国际化
migrations.AlterField( migrations.AlterField(
model_name='article', model_name='article',
name='show_toc', name='show_toc',
field=models.BooleanField(default=False, verbose_name='show toc'), field=models.BooleanField(default=False, verbose_name='show toc'),
), ),
# 修改Article模型的status字段verbose_name和选项国际化
migrations.AlterField( migrations.AlterField(
model_name='article', model_name='article',
name='status', name='status',
field=models.CharField(choices=[('d', 'Draft'), ('p', 'Published')], default='p', max_length=1, verbose_name='status'), field=models.CharField(choices=[('d', 'Draft'), ('p', 'Published')], default='p', max_length=1, verbose_name='status'),
), ),
# 修改Article模型的tags字段verbose_name国际化
migrations.AlterField( migrations.AlterField(
model_name='article', model_name='article',
name='tags', name='tags',
field=models.ManyToManyField(blank=True, to='blog.tag', verbose_name='tag'), field=models.ManyToManyField(blank=True, to='blog.tag', verbose_name='tag'),
), ),
# 修改Article模型的title字段verbose_name国际化
migrations.AlterField( migrations.AlterField(
model_name='article', model_name='article',
name='title', name='title',
field=models.CharField(max_length=200, unique=True, verbose_name='title'), field=models.CharField(max_length=200, unique=True, verbose_name='title'),
), ),
# 修改Article模型的type字段verbose_name和选项国际化
migrations.AlterField( migrations.AlterField(
model_name='article', model_name='article',
name='type', name='type',
field=models.CharField(choices=[('a', 'Article'), ('p', 'Page')], default='a', max_length=1, verbose_name='type'), field=models.CharField(choices=[('a', 'Article'), ('p', 'Page')], default='a', max_length=1, verbose_name='type'),
), ),
# 修改Article模型的views字段verbose_name国际化
migrations.AlterField( migrations.AlterField(
model_name='article', model_name='article',
name='views', name='views',
field=models.PositiveIntegerField(default=0, verbose_name='views'), field=models.PositiveIntegerField(default=0, verbose_name='views'),
), ),
# 修改BlogSettings模型的article_comment_count字段verbose_name国际化
migrations.AlterField( migrations.AlterField(
model_name='blogsettings', model_name='blogsettings',
name='article_comment_count', name='article_comment_count',
field=models.IntegerField(default=5, verbose_name='article comment count'), field=models.IntegerField(default=5, verbose_name='article comment count'),
), ),
# 修改BlogSettings模型的article_sub_length字段verbose_name国际化
migrations.AlterField( migrations.AlterField(
model_name='blogsettings', model_name='blogsettings',
name='article_sub_length', name='article_sub_length',
field=models.IntegerField(default=300, verbose_name='article sub length'), field=models.IntegerField(default=300, verbose_name='article sub length'),
), ),
# 修改BlogSettings模型的google_adsense_codes字段verbose_name国际化
migrations.AlterField( migrations.AlterField(
model_name='blogsettings', model_name='blogsettings',
name='google_adsense_codes', name='google_adsense_codes',
field=models.TextField(blank=True, default='', max_length=2000, null=True, verbose_name='adsense code'), field=models.TextField(blank=True, default='', max_length=2000, null=True, verbose_name='adsense code'),
), ),
# 修改BlogSettings模型的open_site_comment字段verbose_name国际化
migrations.AlterField( migrations.AlterField(
model_name='blogsettings', model_name='blogsettings',
name='open_site_comment', name='open_site_comment',
field=models.BooleanField(default=True, verbose_name='open site comment'), field=models.BooleanField(default=True, verbose_name='open site comment'),
), ),
# 修改BlogSettings模型的show_google_adsense字段verbose_name国际化
migrations.AlterField( migrations.AlterField(
model_name='blogsettings', model_name='blogsettings',
name='show_google_adsense', name='show_google_adsense',
field=models.BooleanField(default=False, verbose_name='show adsense'), field=models.BooleanField(default=False, verbose_name='show adsense'),
), ),
# 修改BlogSettings模型的sidebar_article_count字段verbose_name国际化
migrations.AlterField( migrations.AlterField(
model_name='blogsettings', model_name='blogsettings',
name='sidebar_article_count', name='sidebar_article_count',
field=models.IntegerField(default=10, verbose_name='sidebar article count'), field=models.IntegerField(default=10, verbose_name='sidebar article count'),
), ),
# 修改BlogSettings模型的sidebar_comment_count字段verbose_name国际化
migrations.AlterField( migrations.AlterField(
model_name='blogsettings', model_name='blogsettings',
name='sidebar_comment_count', name='sidebar_comment_count',
field=models.IntegerField(default=5, verbose_name='sidebar comment count'), field=models.IntegerField(default=5, verbose_name='sidebar comment count'),
), ),
# 修改BlogSettings模型的site_description字段verbose_name国际化
migrations.AlterField( migrations.AlterField(
model_name='blogsettings', model_name='blogsettings',
name='site_description', name='site_description',
field=models.TextField(default='', max_length=1000, verbose_name='site description'), field=models.TextField(default='', max_length=1000, verbose_name='site description'),
), ),
# 修改BlogSettings模型的site_keywords字段verbose_name国际化
migrations.AlterField( migrations.AlterField(
model_name='blogsettings', model_name='blogsettings',
name='site_keywords', name='site_keywords',
field=models.TextField(default='', max_length=1000, verbose_name='site keywords'), field=models.TextField(default='', max_length=1000, verbose_name='site keywords'),
), ),
# 修改BlogSettings模型的site_name字段verbose_name国际化
migrations.AlterField( migrations.AlterField(
model_name='blogsettings', model_name='blogsettings',
name='site_name', name='site_name',
field=models.CharField(default='', max_length=200, verbose_name='site name'), field=models.CharField(default='', max_length=200, verbose_name='site name'),
), ),
# 修改BlogSettings模型的site_seo_description字段verbose_name国际化
migrations.AlterField( migrations.AlterField(
model_name='blogsettings', model_name='blogsettings',
name='site_seo_description', name='site_seo_description',
field=models.TextField(default='', max_length=1000, verbose_name='site seo description'), field=models.TextField(default='', max_length=1000, verbose_name='site seo description'),
), ),
# 修改Category模型的index字段verbose_name国际化
migrations.AlterField( migrations.AlterField(
model_name='category', model_name='category',
name='index', name='index',
field=models.IntegerField(default=0, verbose_name='index'), field=models.IntegerField(default=0, verbose_name='index'),
), ),
# 修改Category模型的name字段verbose_name国际化
migrations.AlterField( migrations.AlterField(
model_name='category', model_name='category',
name='name', name='name',
field=models.CharField(max_length=30, unique=True, verbose_name='category name'), field=models.CharField(max_length=30, unique=True, verbose_name='category name'),
), ),
# 修改Category模型的parent_category字段verbose_name国际化
migrations.AlterField( migrations.AlterField(
model_name='category', model_name='category',
name='parent_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'), 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( migrations.AlterField(
model_name='links', model_name='links',
name='is_enable', name='is_enable',
field=models.BooleanField(default=True, verbose_name='is show'), field=models.BooleanField(default=True, verbose_name='is show'),
), ),
# 修改Links模型的last_mod_time字段verbose_name国际化
migrations.AlterField( migrations.AlterField(
model_name='links', model_name='links',
name='last_mod_time', name='last_mod_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'), field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
), ),
# 修改Links模型的link字段verbose_name国际化
migrations.AlterField( migrations.AlterField(
model_name='links', model_name='links',
name='link', name='link',
field=models.URLField(verbose_name='link'), field=models.URLField(verbose_name='link'),
), ),
# 修改Links模型的name字段verbose_name国际化
migrations.AlterField( migrations.AlterField(
model_name='links', model_name='links',
name='name', name='name',
field=models.CharField(max_length=30, unique=True, verbose_name='link name'), field=models.CharField(max_length=30, unique=True, verbose_name='link name'),
), ),
# 修改Links模型的sequence字段verbose_name国际化
migrations.AlterField( migrations.AlterField(
model_name='links', model_name='links',
name='sequence', name='sequence',
field=models.IntegerField(unique=True, verbose_name='order'), field=models.IntegerField(unique=True, verbose_name='order'),
), ),
# 修改Links模型的show_type字段verbose_name和选项国际化
migrations.AlterField( migrations.AlterField(
model_name='links', model_name='links',
name='show_type', 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'), 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( migrations.AlterField(
model_name='sidebar', model_name='sidebar',
name='content', name='content',
field=models.TextField(verbose_name='content'), field=models.TextField(verbose_name='content'),
), ),
# 修改Sidebar模型的is_enable字段verbose_name国际化
migrations.AlterField( migrations.AlterField(
model_name='sidebar', model_name='sidebar',
name='is_enable', name='is_enable',
field=models.BooleanField(default=True, verbose_name='is enable'), field=models.BooleanField(default=True, verbose_name='is enable'),
), ),
# 修改Sidebar模型的last_mod_time字段verbose_name国际化
migrations.AlterField( migrations.AlterField(
model_name='sidebar', model_name='sidebar',
name='last_mod_time', name='last_mod_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'), field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
), ),
# 修改Sidebar模型的name字段verbose_name国际化
migrations.AlterField( migrations.AlterField(
model_name='sidebar', model_name='sidebar',
name='name', name='name',
field=models.CharField(max_length=100, verbose_name='title'), field=models.CharField(max_length=100, verbose_name='title'),
), ),
# 修改Sidebar模型的sequence字段verbose_name国际化
migrations.AlterField( migrations.AlterField(
model_name='sidebar', model_name='sidebar',
name='sequence', name='sequence',
field=models.IntegerField(unique=True, verbose_name='order'), field=models.IntegerField(unique=True, verbose_name='order'),
), ),
# 修改Tag模型的name字段verbose_name国际化
migrations.AlterField( migrations.AlterField(
model_name='tag', model_name='tag',
name='name', name='name',
field=models.CharField(max_length=30, unique=True, verbose_name='tag name'), field=models.CharField(max_length=30, unique=True, verbose_name='tag name'),
), ),
] ]

@ -8,7 +8,22 @@ class Migration(migrations.Migration):
dependencies = [ dependencies = [
('blog', '0005_alter_article_options_alter_category_options_and_more'), ('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 = [ operations = [
migrations.AlterModelOptions( migrations.AlterModelOptions(
name='blogsettings', name='blogsettings',

@ -18,106 +18,114 @@ logger = logging.getLogger(__name__)
class LinkShowType(models.TextChoices): class LinkShowType(models.TextChoices):
I = ('i', _('index')) # 链接显示类型选择
L = ('l', _('list')) I = ('i', _('index')) # 首页显示
P = ('p', _('post')) L = ('l', _('list')) # 列表页显示
A = ('a', _('all')) P = ('p', _('post')) # 文章页面显示
S = ('s', _('slide')) A = ('a', _('all')) # 全站显示
S = ('s', _('slide')) # 幻灯片显示
class BaseModel(models.Model): class BaseModel(models.Model):
id = models.AutoField(primary_key=True) # 基础模型类,提供公共字段和方法
creation_time = models.DateTimeField(_('creation time'), default=now) id = models.AutoField(primary_key=True) # 自增主键
last_modify_time = models.DateTimeField(_('modify time'), default=now) creation_time = models.DateTimeField(_('creation time'), default=now) # 创建时间
last_modify_time = models.DateTimeField(_('modify time'), default=now) # 最后修改时间
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
# 重写保存方法,处理特殊逻辑
is_update_views = isinstance( is_update_views = isinstance(
self, 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: 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: else:
if 'slug' in self.__dict__: if 'slug' in self.__dict__: # 如果有slug字段
slug = getattr( slug = getattr(
self, 'title') if 'title' in self.__dict__ else getattr( self, 'title') if 'title' in self.__dict__ else getattr(
self, 'name') self, 'name') # 根据title或name生成slug
setattr(self, 'slug', slugify(slug)) setattr(self, 'slug', slugify(slug)) # 设置slug值
super().save(*args, **kwargs) super().save(*args, **kwargs) # 调用父类保存方法
def get_full_url(self): def get_full_url(self):
site = get_current_site().domain # 获取完整URL
site = get_current_site().domain # 获取当前站点域名
url = "https://{site}{path}".format(site=site, url = "https://{site}{path}".format(site=site,
path=self.get_absolute_url()) path=self.get_absolute_url()) # 构建完整URL
return url return url
class Meta: class Meta:
abstract = True abstract = True # 抽象基类,不会创建数据库表
@abstractmethod @abstractmethod
def get_absolute_url(self): def get_absolute_url(self):
# 抽象方法子类必须实现获取绝对URL的方法
pass pass
class Article(BaseModel): class Article(BaseModel):
"""文章""" """文章模型"""
STATUS_CHOICES = ( STATUS_CHOICES = (
('d', _('Draft')), ('d', _('Draft')), # 草稿状态
('p', _('Published')), ('p', _('Published')), # 已发布状态
) )
COMMENT_STATUS = ( COMMENT_STATUS = (
('o', _('Open')), ('o', _('Open')), # 评论开启
('c', _('Close')), ('c', _('Close')), # 评论关闭
) )
TYPE = ( TYPE = (
('a', _('Article')), ('a', _('Article')), # 文章类型
('p', _('Page')), ('p', _('Page')), # 页面类型
) )
title = models.CharField(_('title'), max_length=200, unique=True) title = models.CharField(_('title'), max_length=200, unique=True) # 文章标题
body = MDTextField(_('body')) body = MDTextField(_('body')) # 文章正文使用Markdown编辑器
pub_time = models.DateTimeField( pub_time = models.DateTimeField(
_('publish time'), blank=False, null=False, default=now) _('publish time'), blank=False, null=False, default=now) # 发布时间
status = models.CharField( status = models.CharField(
_('status'), _('status'),
max_length=1, max_length=1,
choices=STATUS_CHOICES, choices=STATUS_CHOICES,
default='p') default='p') # 文章状态
comment_status = models.CharField( comment_status = models.CharField(
_('comment status'), _('comment status'),
max_length=1, max_length=1,
choices=COMMENT_STATUS, choices=COMMENT_STATUS,
default='o') default='o') # 评论状态
type = models.CharField(_('type'), max_length=1, choices=TYPE, default='a') type = models.CharField(_('type'), max_length=1, choices=TYPE, default='a') # 文章类型
views = models.PositiveIntegerField(_('views'), default=0) views = models.PositiveIntegerField(_('views'), default=0) # 浏览量
author = models.ForeignKey( author = models.ForeignKey(
settings.AUTH_USER_MODEL, settings.AUTH_USER_MODEL,
verbose_name=_('author'), verbose_name=_('author'),
blank=False, blank=False,
null=False, null=False,
on_delete=models.CASCADE) on_delete=models.CASCADE) # 作者外键
article_order = models.IntegerField( article_order = models.IntegerField(
_('order'), blank=False, null=False, default=0) _('order'), blank=False, null=False, default=0) # 文章排序
show_toc = models.BooleanField(_('show toc'), blank=False, null=False, default=False) show_toc = models.BooleanField(_('show toc'), blank=False, null=False, default=False) # 是否显示目录
category = models.ForeignKey( category = models.ForeignKey(
'Category', 'Category',
verbose_name=_('category'), verbose_name=_('category'),
on_delete=models.CASCADE, on_delete=models.CASCADE,
blank=False, blank=False,
null=False) null=False) # 分类外键
tags = models.ManyToManyField('Tag', verbose_name=_('tag'), blank=True) tags = models.ManyToManyField('Tag', verbose_name=_('tag'), blank=True) # 标签多对多关系
def body_to_string(self): def body_to_string(self):
# 返回正文字符串
return self.body return self.body
def __str__(self): def __str__(self):
# 对象字符串表示
return self.title return self.title
class Meta: class Meta:
ordering = ['-article_order', '-pub_time'] ordering = ['-article_order', '-pub_time'] # 默认排序:按文章排序倒序,发布时间倒序
verbose_name = _('article') verbose_name = _('article') # 单数显示名称
verbose_name_plural = verbose_name verbose_name_plural = verbose_name # 复数显示名称
get_latest_by = 'id' get_latest_by = 'id' # 指定获取最新记录的字段
def get_absolute_url(self): def get_absolute_url(self):
# 获取文章绝对URL
return reverse('blog:detailbyid', kwargs={ return reverse('blog:detailbyid', kwargs={
'article_id': self.id, 'article_id': self.id,
'year': self.creation_time.year, 'year': self.creation_time.year,
@ -125,252 +133,269 @@ class Article(BaseModel):
'day': self.creation_time.day 'day': self.creation_time.day
}) })
@cache_decorator(60 * 60 * 10) @cache_decorator(60 * 60 * 10) # 缓存10小时
def get_category_tree(self): 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 return names
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
# 保存文章
super().save(*args, **kwargs) super().save(*args, **kwargs)
def viewed(self): def viewed(self):
# 增加浏览量
self.views += 1 self.views += 1
self.save(update_fields=['views']) self.save(update_fields=['views']) # 只更新views字段
def comment_list(self): 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: if value:
logger.info('get article comments:{id}'.format(id=self.id)) logger.info('get article comments:{id}'.format(id=self.id)) # 记录缓存命中日志
return value return value
else: else:
comments = self.comment_set.filter(is_enable=True).order_by('-id') comments = self.comment_set.filter(is_enable=True).order_by('-id') # 获取有效评论并按ID倒序
cache.set(cache_key, comments, 60 * 100) cache.set(cache_key, comments, 60 * 100) # 设置缓存有效期100分钟
logger.info('set article comments:{id}'.format(id=self.id)) logger.info('set article comments:{id}'.format(id=self.id)) # 记录缓存设置日志
return comments return comments
def get_admin_url(self): def get_admin_url(self):
info = (self._meta.app_label, self._meta.model_name) # 获取管理后台URL
return reverse('admin:%s_%s_change' % info, args=(self.pk,)) 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): def next_article(self):
# 下一篇 # 获取下一篇文章
return Article.objects.filter( 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): 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): def get_first_image_url(self):
""" """
Get the first image url from article.body. 从文章正文中获取第一张图片URL
:return: :return:
""" """
match = re.search(r'!\[.*?\]\((.+?)\)', self.body) match = re.search(r'!\[.*?\]\((.+?)\)', self.body) # 使用正则表达式匹配Markdown图片语法
if match: if match:
return match.group(1) return match.group(1) # 返回图片URL
return "" return "" # 没有图片返回空字符串
class Category(BaseModel): 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( parent_category = models.ForeignKey(
'self', 'self',
verbose_name=_('parent category'), verbose_name=_('parent category'),
blank=True, blank=True,
null=True, null=True,
on_delete=models.CASCADE) on_delete=models.CASCADE) # 父级分类,自关联
slug = models.SlugField(default='no-slug', max_length=60, blank=True) slug = models.SlugField(default='no-slug', max_length=60, blank=True) # URL友好标识
index = models.IntegerField(default=0, verbose_name=_('index')) index = models.IntegerField(default=0, verbose_name=_('index')) # 排序索引
class Meta: class Meta:
ordering = ['-index'] ordering = ['-index'] # 默认按索引倒序排列
verbose_name = _('category') verbose_name = _('category') # 单数显示名称
verbose_name_plural = verbose_name verbose_name_plural = verbose_name # 复数显示名称
def get_absolute_url(self): def get_absolute_url(self):
# 获取分类绝对URL
return reverse( return reverse(
'blog:category_detail', kwargs={ 'blog:category_detail', kwargs={
'category_name': self.slug}) 'category_name': self.slug}) # 使用slug作为URL参数
def __str__(self): def __str__(self):
# 对象字符串表示
return self.name return self.name
@cache_decorator(60 * 60 * 10) @cache_decorator(60 * 60 * 10) # 缓存10小时
def get_category_tree(self): def get_category_tree(self):
""" """
递归获得分类目录的父级 递归获得分类目录的父级
:return: :return:
""" """
categorys = [] categorys = [] # 存储分类树
def parse(category): def parse(category):
categorys.append(category) # 递归解析分类树
if category.parent_category: categorys.append(category) # 添加当前分类
parse(category.parent_category) if category.parent_category: # 如果有父级分类
parse(category.parent_category) # 递归解析父级分类
parse(self) parse(self) # 从当前分类开始解析
return categorys return categorys
@cache_decorator(60 * 60 * 10) @cache_decorator(60 * 60 * 10) # 缓存10小时
def get_sub_categorys(self): def get_sub_categorys(self):
""" """
获得当前分类目录所有子集 获得当前分类目录所有子集
:return: :return:
""" """
categorys = [] categorys = [] # 存储子分类
all_categorys = Category.objects.all() all_categorys = Category.objects.all() # 获取所有分类
def parse(category): def parse(category):
# 递归解析子分类
if category not in categorys: if category not in categorys:
categorys.append(category) categorys.append(category) # 添加当前分类
childs = all_categorys.filter(parent_category=category) childs = all_categorys.filter(parent_category=category) # 获取直接子分类
for child in childs: for child in childs:
if category not in categorys: if category not in categorys:
categorys.append(child) categorys.append(child) # 添加子分类
parse(child) parse(child) # 递归解析子分类的子分类
parse(self) parse(self) # 从当前分类开始解析
return categorys return categorys
class Tag(BaseModel): class Tag(BaseModel):
"""文章标签""" """文章标签模型"""
name = models.CharField(_('tag name'), max_length=30, unique=True) name = models.CharField(_('tag name'), max_length=30, unique=True) # 标签名称
slug = models.SlugField(default='no-slug', max_length=60, blank=True) slug = models.SlugField(default='no-slug', max_length=60, blank=True) # URL友好标识
def __str__(self): def __str__(self):
# 对象字符串表示
return self.name return self.name
def get_absolute_url(self): 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): 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: class Meta:
ordering = ['name'] ordering = ['name'] # 默认按名称升序排列
verbose_name = _('tag') verbose_name = _('tag') # 单数显示名称
verbose_name_plural = verbose_name verbose_name_plural = verbose_name # 复数显示名称
class Links(models.Model): class Links(models.Model):
"""友情链接""" """友情链接模型"""
name = models.CharField(_('link name'), max_length=30, unique=True) name = models.CharField(_('link name'), max_length=30, unique=True) # 链接名称
link = models.URLField(_('link')) link = models.URLField(_('link')) # 链接地址
sequence = models.IntegerField(_('order'), unique=True) sequence = models.IntegerField(_('order'), unique=True) # 排序序号
is_enable = models.BooleanField( 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 = models.CharField(
_('show type'), _('show type'),
max_length=1, max_length=1,
choices=LinkShowType.choices, choices=LinkShowType.choices,
default=LinkShowType.I) default=LinkShowType.I) # 显示类型
creation_time = models.DateTimeField(_('creation time'), default=now) creation_time = models.DateTimeField(_('creation time'), default=now) # 创建时间
last_mod_time = models.DateTimeField(_('modify time'), default=now) last_mod_time = models.DateTimeField(_('modify time'), default=now) # 最后修改时间
class Meta: class Meta:
ordering = ['sequence'] ordering = ['sequence'] # 默认按序号升序排列
verbose_name = _('link') verbose_name = _('link') # 单数显示名称
verbose_name_plural = verbose_name verbose_name_plural = verbose_name # 复数显示名称
def __str__(self): def __str__(self):
# 对象字符串表示
return self.name return self.name
class SideBar(models.Model): class SideBar(models.Model):
"""侧边栏,可以展示一些html内容""" """侧边栏模型,可以展示一些html内容"""
name = models.CharField(_('title'), max_length=100) name = models.CharField(_('title'), max_length=100) # 侧边栏标题
content = models.TextField(_('content')) content = models.TextField(_('content')) # 侧边栏内容
sequence = models.IntegerField(_('order'), unique=True) sequence = models.IntegerField(_('order'), unique=True) # 排序序号
is_enable = models.BooleanField(_('is enable'), default=True) is_enable = models.BooleanField(_('is enable'), default=True) # 是否启用
creation_time = models.DateTimeField(_('creation time'), default=now) creation_time = models.DateTimeField(_('creation time'), default=now) # 创建时间
last_mod_time = models.DateTimeField(_('modify time'), default=now) last_mod_time = models.DateTimeField(_('modify time'), default=now) # 最后修改时间
class Meta: class Meta:
ordering = ['sequence'] ordering = ['sequence'] # 默认按序号升序排列
verbose_name = _('sidebar') verbose_name = _('sidebar') # 单数显示名称
verbose_name_plural = verbose_name verbose_name_plural = verbose_name # 复数显示名称
def __str__(self): def __str__(self):
# 对象字符串表示
return self.name return self.name
class BlogSettings(models.Model): class BlogSettings(models.Model):
"""blog的配置""" """博客配置模型"""
site_name = models.CharField( site_name = models.CharField(
_('site name'), _('site name'),
max_length=200, max_length=200,
null=False, null=False,
blank=False, blank=False,
default='') default='') # 网站名称
site_description = models.TextField( site_description = models.TextField(
_('site description'), _('site description'),
max_length=1000, max_length=1000,
null=False, null=False,
blank=False, blank=False,
default='') default='') # 网站描述
site_seo_description = models.TextField( 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 = models.TextField(
_('site keywords'), _('site keywords'),
max_length=1000, max_length=1000,
null=False, null=False,
blank=False, blank=False,
default='') default='') # 网站关键词
article_sub_length = models.IntegerField(_('article sub length'), default=300) article_sub_length = models.IntegerField(_('article sub length'), default=300) # 文章摘要长度
sidebar_article_count = models.IntegerField(_('sidebar article count'), default=10) sidebar_article_count = models.IntegerField(_('sidebar article count'), default=10) # 侧边栏文章数量
sidebar_comment_count = models.IntegerField(_('sidebar comment count'), default=5) sidebar_comment_count = models.IntegerField(_('sidebar comment count'), default=5) # 侧边栏评论数量
article_comment_count = models.IntegerField(_('article comment count'), default=5) article_comment_count = models.IntegerField(_('article comment count'), default=5) # 文章页面评论数量
show_google_adsense = models.BooleanField(_('show adsense'), default=False) show_google_adsense = models.BooleanField(_('show adsense'), default=False) # 是否显示Google广告
google_adsense_codes = models.TextField( google_adsense_codes = models.TextField(
_('adsense code'), max_length=2000, 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) open_site_comment = models.BooleanField(_('open site comment'), default=True) # 是否开启网站评论
global_header = models.TextField("公共头部", null=True, blank=True, default='') global_header = models.TextField("公共头部", null=True, blank=True, default='') # 全局头部内容
global_footer = models.TextField("公共尾部", null=True, blank=True, default='') global_footer = models.TextField("公共尾部", null=True, blank=True, default='') # 全局尾部内容
beian_code = models.CharField( beian_code = models.CharField(
'备案号', '备案号',
max_length=2000, max_length=2000,
null=True, null=True,
blank=True, blank=True,
default='') default='') # 备案号
analytics_code = models.TextField( analytics_code = models.TextField(
"网站统计代码", "网站统计代码",
max_length=1000, max_length=1000,
null=False, null=False,
blank=False, blank=False,
default='') default='') # 网站统计代码
show_gongan_code = models.BooleanField( show_gongan_code = models.BooleanField(
'是否显示公安备案号', default=False, null=False) '是否显示公安备案号', default=False, null=False) # 是否显示公安备案号
gongan_beiancode = models.TextField( gongan_beiancode = models.TextField(
'公安备案号', '公安备案号',
max_length=2000, max_length=2000,
null=True, null=True,
blank=True, blank=True,
default='') default='') # 公安备案号
comment_need_review = models.BooleanField( comment_need_review = models.BooleanField(
'评论是否需要审核', default=False, null=False) '评论是否需要审核', default=False, null=False) # 评论是否需要审核
class Meta: class Meta:
verbose_name = _('Website configuration') verbose_name = _('Website configuration') # 单数显示名称
verbose_name_plural = verbose_name verbose_name_plural = verbose_name # 复数显示名称
def __str__(self): def __str__(self):
# 对象字符串表示
return self.site_name return self.site_name
def clean(self): def clean(self):
# 数据清理验证,确保只能有一个配置实例
if BlogSettings.objects.exclude(id=self.id).count(): 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): def save(self, *args, **kwargs):
# 保存配置,同时清除缓存
super().save(*args, **kwargs) super().save(*args, **kwargs)
from djangoblog.utils import cache from djangoblog.utils import cache
cache.clear() cache.clear() # 清除所有缓存

@ -4,10 +4,13 @@ from blog.models import Article
class ArticleIndex(indexes.SearchIndex, indexes.Indexable): 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): def get_model(self):
# 返回与此索引关联的模型类
return Article return Article
def index_queryset(self, using=None): def index_queryset(self, using=None):
return self.get_model().objects.filter(status='p') # 返回要建立索引的查询集,只包含已发布的文章
return self.get_model().objects.filter(status='p')

@ -22,18 +22,20 @@ from djangoblog.plugin_manage import hooks
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
register = template.Library() register = template.Library() # 创建模板标签库实例
@register.simple_tag(takes_context=True) @register.simple_tag(takes_context=True)
def head_meta(context): def head_meta(context):
# 头部meta标签应用插件过滤器
return mark_safe(hooks.apply_filters('head_meta', '', context)) return mark_safe(hooks.apply_filters('head_meta', '', context))
@register.simple_tag @register.simple_tag
def timeformat(data): def timeformat(data):
# 时间格式化标签
try: try:
return data.strftime(settings.TIME_FORMAT) return data.strftime(settings.TIME_FORMAT) # 使用设置中的时间格式
except Exception as e: except Exception as e:
logger.error(e) logger.error(e)
return "" return ""
@ -41,8 +43,9 @@ def timeformat(data):
@register.simple_tag @register.simple_tag
def datetimeformat(data): def datetimeformat(data):
# 日期时间格式化标签
try: try:
return data.strftime(settings.DATE_TIME_FORMAT) return data.strftime(settings.DATE_TIME_FORMAT) # 使用设置中的日期时间格式
except Exception as e: except Exception as e:
logger.error(e) logger.error(e)
return "" return ""
@ -55,19 +58,20 @@ def custom_markdown(content):
通用markdown过滤器应用文章内容插件 通用markdown过滤器应用文章内容插件
主要用于文章内容处理 主要用于文章内容处理
""" """
html_content = CommonMarkdown.get_markdown(content) html_content = CommonMarkdown.get_markdown(content) # 将markdown转换为HTML
# 然后应用插件过滤器优化HTML # 然后应用插件过滤器优化HTML
from djangoblog.plugin_manage import hooks from djangoblog.plugin_manage import hooks
from djangoblog.plugin_manage.hook_constants import ARTICLE_CONTENT_HOOK_NAME from djangoblog.plugin_manage.hook_constants import ARTICLE_CONTENT_HOOK_NAME
optimized_html = hooks.apply_filters(ARTICLE_CONTENT_HOOK_NAME, html_content) optimized_html = hooks.apply_filters(ARTICLE_CONTENT_HOOK_NAME, html_content) # 应用文章内容插件
return mark_safe(optimized_html) return mark_safe(optimized_html) # 标记为安全HTML
@register.filter() @register.filter()
@stringfilter @stringfilter
def sidebar_markdown(content): def sidebar_markdown(content):
# 侧边栏markdown过滤器
html_content = CommonMarkdown.get_markdown(content) html_content = CommonMarkdown.get_markdown(content)
return mark_safe(html_content) return mark_safe(html_content)
@ -76,7 +80,7 @@ def sidebar_markdown(content):
def render_article_content(context, article, is_summary=False): def render_article_content(context, article, is_summary=False):
""" """
渲染文章内容包含完整的上下文信息供插件使用 渲染文章内容包含完整的上下文信息供插件使用
Args: Args:
context: 模板上下文 context: 模板上下文
article: 文章对象 article: 文章对象
@ -84,56 +88,58 @@ def render_article_content(context, article, is_summary=False):
""" """
if not article or not hasattr(article, 'body'): if not article or not hasattr(article, 'body'):
return '' return ''
# 先转换Markdown为HTML # 先转换Markdown为HTML
html_content = CommonMarkdown.get_markdown(article.body) html_content = CommonMarkdown.get_markdown(article.body)
# 如果是摘要模式,先截断内容再应用插件 # 如果是摘要模式,先截断内容再应用插件
if is_summary: if is_summary:
# 截断HTML内容到合适的长度约300字符 # 截断HTML内容到合适的长度约300字符
from django.utils.html import strip_tags from django.utils.html import strip_tags
from django.template.defaultfilters import truncatechars from django.template.defaultfilters import truncatechars
# 先去除HTML标签截断纯文本然后重新转换为HTML # 先去除HTML标签截断纯文本然后重新转换为HTML
plain_text = strip_tags(html_content) plain_text = strip_tags(html_content)
truncated_text = truncatechars(plain_text, 300) truncated_text = truncatechars(plain_text, 300)
# 重新转换截断后的文本为HTML简化版避免复杂的插件处理 # 重新转换截断后的文本为HTML简化版避免复杂的插件处理
html_content = CommonMarkdown.get_markdown(truncated_text) html_content = CommonMarkdown.get_markdown(truncated_text)
# 然后应用插件过滤器,传递完整的上下文 # 然后应用插件过滤器,传递完整的上下文
from djangoblog.plugin_manage import hooks from djangoblog.plugin_manage import hooks
from djangoblog.plugin_manage.hook_constants import ARTICLE_CONTENT_HOOK_NAME from djangoblog.plugin_manage.hook_constants import ARTICLE_CONTENT_HOOK_NAME
# 获取request对象 # 获取request对象
request = context.get('request') request = context.get('request')
# 应用所有文章内容相关的插件 # 应用所有文章内容相关的插件
# 注意:摘要模式下某些插件(如版权声明)可能不适用 # 注意:摘要模式下某些插件(如版权声明)可能不适用
optimized_html = hooks.apply_filters( optimized_html = hooks.apply_filters(
ARTICLE_CONTENT_HOOK_NAME, ARTICLE_CONTENT_HOOK_NAME,
html_content, html_content,
article=article, article=article,
request=request, request=request,
context=context, context=context,
is_summary=is_summary # 传递摘要标志,插件可以据此调整行为 is_summary=is_summary # 传递摘要标志,插件可以据此调整行为
) )
return mark_safe(optimized_html) return mark_safe(optimized_html)
@register.simple_tag @register.simple_tag
def get_markdown_toc(content): def get_markdown_toc(content):
# 获取markdown目录
from djangoblog.utils import CommonMarkdown 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) return mark_safe(toc)
@register.filter() @register.filter()
@stringfilter @stringfilter
def comment_markdown(content): def comment_markdown(content):
# 评论markdown过滤器并进行HTML清理
content = CommonMarkdown.get_markdown(content) content = CommonMarkdown.get_markdown(content)
return mark_safe(sanitize_html(content)) return mark_safe(sanitize_html(content)) # 清理不安全的HTML
@register.filter(is_safe=True) @register.filter(is_safe=True)
@ -146,13 +152,14 @@ def truncatechars_content(content):
""" """
from django.template.defaultfilters import truncatechars_html from django.template.defaultfilters import truncatechars_html
from djangoblog.utils import get_blog_setting from djangoblog.utils import get_blog_setting
blogsetting = get_blog_setting() blogsetting = get_blog_setting() # 获取博客设置
return truncatechars_html(content, blogsetting.article_sub_length) return truncatechars_html(content, blogsetting.article_sub_length) # 根据设置截断HTML内容
@register.filter(is_safe=True) @register.filter(is_safe=True)
@stringfilter @stringfilter
def truncate(content): def truncate(content):
# 简单截断过滤器去除HTML标签后截取前150字符
from django.utils.html import strip_tags from django.utils.html import strip_tags
return strip_tags(content)[:150] return strip_tags(content)[:150]
@ -165,12 +172,12 @@ def load_breadcrumb(article):
:param article: :param article:
:return: :return:
""" """
names = article.get_category_tree() names = article.get_category_tree() # 获取分类树
from djangoblog.utils import get_blog_setting from djangoblog.utils import get_blog_setting
blogsetting = get_blog_setting() blogsetting = get_blog_setting()
site = get_current_site().domain site = get_current_site().domain
names.append((blogsetting.site_name, '/')) names.append((blogsetting.site_name, '/')) # 添加网站首页
names = names[::-1] names = names[::-1] # 反转列表顺序
return { return {
'names': names, 'names': names,
@ -189,10 +196,10 @@ def load_articletags(article):
tags = article.tags.all() tags = article.tags.all()
tags_list = [] tags_list = []
for tag in tags: for tag in tags:
url = tag.get_absolute_url() url = tag.get_absolute_url() # 获取标签URL
count = tag.get_article_count() count = tag.get_article_count() # 获取标签下文章数量
tags_list.append(( tags_list.append((
url, count, tag, random.choice(settings.BOOTSTRAP_COLOR_TYPES) url, count, tag, random.choice(settings.BOOTSTRAP_COLOR_TYPES) # 随机选择Bootstrap颜色
)) ))
return { return {
'article_tags_list': tags_list 'article_tags_list': tags_list
@ -205,7 +212,7 @@ def load_sidebar(user, linktype):
加载侧边栏 加载侧边栏
:return: :return:
""" """
value = cache.get("sidebar" + linktype) value = cache.get("sidebar" + linktype) # 尝试从缓存获取侧边栏
if value: if value:
value['user'] = user value['user'] = user
return value return value
@ -214,30 +221,30 @@ def load_sidebar(user, linktype):
from djangoblog.utils import get_blog_setting from djangoblog.utils import get_blog_setting
blogsetting = get_blog_setting() blogsetting = get_blog_setting()
recent_articles = Article.objects.filter( recent_articles = Article.objects.filter(
status='p')[:blogsetting.sidebar_article_count] status='p')[:blogsetting.sidebar_article_count] # 最近文章
sidebar_categorys = Category.objects.all() sidebar_categorys = Category.objects.all() # 全部分类
extra_sidebars = SideBar.objects.filter( 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( most_read_articles = Article.objects.filter(status='p').order_by(
'-views')[:blogsetting.sidebar_article_count] '-views')[:blogsetting.sidebar_article_count] # 最多阅读文章
dates = Article.objects.datetimes('creation_time', 'month', order='DESC') dates = Article.objects.datetimes('creation_time', 'month', order='DESC') # 文章归档日期
links = Links.objects.filter(is_enable=True).filter( 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( commment_list = Comment.objects.filter(is_enable=True).order_by(
'-id')[:blogsetting.sidebar_comment_count] '-id')[:blogsetting.sidebar_comment_count] # 最新评论
# 标签云 计算字体大小 # 标签云 计算字体大小
# 根据总数计算出平均值 大小为 (数目/平均值)*步长 # 根据总数计算出平均值 大小为 (数目/平均值)*步长
increment = 5 increment = 5
tags = Tag.objects.all() tags = Tag.objects.all()
sidebar_tags = None sidebar_tags = None
if tags and len(tags) > 0: if tags and len(tags) > 0:
s = [t for t in [(t, t.get_article_count()) for t in tags] if t[1]] 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]) count = sum([t[1] for t in s]) # 计算总文章数
dd = 1 if (count == 0 or not len(tags)) else count / len(tags) dd = 1 if (count == 0 or not len(tags)) else count / len(tags) # 计算平均值
import random import random
sidebar_tags = list( sidebar_tags = list(
map(lambda x: (x[0], x[1], (x[1] / dd) * increment + 10), s)) map(lambda x: (x[0], x[1], (x[1] / dd) * increment + 10), s)) # 计算标签字体大小
random.shuffle(sidebar_tags) random.shuffle(sidebar_tags) # 随机打乱标签顺序
value = { value = {
'recent_articles': recent_articles, 'recent_articles': recent_articles,
@ -253,7 +260,7 @@ def load_sidebar(user, linktype):
'sidebar_tags': sidebar_tags, 'sidebar_tags': sidebar_tags,
'extra_sidebars': extra_sidebars '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)) logger.info('set sidebar cache.key:{key}'.format(key="sidebar" + linktype))
value['user'] = user value['user'] = user
return value return value
@ -274,9 +281,10 @@ def load_article_metas(article, user):
@register.inclusion_tag('blog/tags/article_pagination.html') @register.inclusion_tag('blog/tags/article_pagination.html')
def load_pagination_info(page_obj, page_type, tag_name): def load_pagination_info(page_obj, page_type, tag_name):
# 分页信息标签
previous_url = '' previous_url = ''
next_url = '' next_url = ''
if page_type == '': if page_type == '': # 首页分页
if page_obj.has_next(): if page_obj.has_next():
next_number = page_obj.next_page_number() next_number = page_obj.next_page_number()
next_url = reverse('blog:index_page', kwargs={'page': next_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( previous_url = reverse(
'blog:index_page', kwargs={ 'blog:index_page', kwargs={
'page': previous_number}) 'page': previous_number})
if page_type == '分类标签归档': if page_type == '分类标签归档': # 标签分页
tag = get_object_or_404(Tag, name=tag_name) tag = get_object_or_404(Tag, name=tag_name)
if page_obj.has_next(): if page_obj.has_next():
next_number = page_obj.next_page_number() next_number = page_obj.next_page_number()
@ -301,7 +309,7 @@ def load_pagination_info(page_obj, page_type, tag_name):
kwargs={ kwargs={
'page': previous_number, 'page': previous_number,
'tag_name': tag.slug}) 'tag_name': tag.slug})
if page_type == '作者文章归档': if page_type == '作者文章归档': # 作者分页
if page_obj.has_next(): if page_obj.has_next():
next_number = page_obj.next_page_number() next_number = page_obj.next_page_number()
next_url = reverse( next_url = reverse(
@ -317,7 +325,7 @@ def load_pagination_info(page_obj, page_type, tag_name):
'page': previous_number, 'page': previous_number,
'author_name': tag_name}) 'author_name': tag_name})
if page_type == '分类目录归档': if page_type == '分类目录归档': # 分类分页
category = get_object_or_404(Category, name=tag_name) category = get_object_or_404(Category, name=tag_name)
if page_obj.has_next(): if page_obj.has_next():
next_number = page_obj.next_page_number() next_number = page_obj.next_page_number()
@ -366,10 +374,10 @@ def load_article_detail(article, isindex, user):
def gravatar_url(email, size=40): def gravatar_url(email, size=40):
"""获得用户头像 - 优先使用OAuth头像否则使用默认头像""" """获得用户头像 - 优先使用OAuth头像否则使用默认头像"""
cachekey = 'avatar/' + email cachekey = 'avatar/' + email
url = cache.get(cachekey) url = cache.get(cachekey) # 尝试从缓存获取头像
if url: if url:
return url return url
# 检查OAuth用户是否有自定义头像 # 检查OAuth用户是否有自定义头像
usermodels = OAuthUser.objects.filter(email=email) usermodels = OAuthUser.objects.filter(email=email)
if usermodels: if usermodels:
@ -378,18 +386,19 @@ def gravatar_url(email, size=40):
if users_with_picture: if users_with_picture:
# 获取默认头像路径用于比较 # 获取默认头像路径用于比较
default_avatar_path = static('blog/img/avatar.png') 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] selected_user = non_default_users[0] if non_default_users else users_with_picture[0]
url = selected_user.picture url = selected_user.picture
cache.set(cachekey, url, 60 * 60 * 24) # 缓存24小时 cache.set(cachekey, url, 60 * 60 * 24) # 缓存24小时
avatar_type = 'non-default' if non_default_users else 'default' avatar_type = 'non-default' if non_default_users else 'default'
logger.info('Using {} OAuth avatar for {} from {}'.format(avatar_type, email, selected_user.type)) logger.info('Using {} OAuth avatar for {} from {}'.format(avatar_type, email, selected_user.type))
return url return url
# 使用默认头像 # 使用默认头像
url = static('blog/img/avatar.png') url = static('blog/img/avatar.png')
cache.set(cachekey, url, 60 * 60 * 24) # 缓存24小时 cache.set(cachekey, url, 60 * 60 * 24) # 缓存24小时
@ -403,7 +412,7 @@ def gravatar(email, size=40):
url = gravatar_url(email, size) url = gravatar_url(email, size)
return mark_safe( return mark_safe(
'<img src="%s" height="%d" width="%d" class="avatar" alt="用户头像">' % '<img src="%s" height="%d" width="%d" class="avatar" alt="用户头像">' %
(url, size, size)) (url, size, size)) # 生成头像img标签
@register.simple_tag @register.simple_tag
@ -414,10 +423,10 @@ def query(qs, **kwargs):
... ...
{% endfor %} {% endfor %}
""" """
return qs.filter(**kwargs) return qs.filter(**kwargs) # 查询集过滤
@register.filter @register.filter
def addstr(arg1, arg2): def addstr(arg1, arg2):
"""concatenate arg1 & arg2""" """concatenate arg1 & arg2"""
return str(arg1) + str(arg2) return str(arg1) + str(arg2) # 字符串连接

@ -21,54 +21,57 @@ from oauth.models import OAuthUser, OAuthConfig
class ArticleTest(TestCase): class ArticleTest(TestCase):
def setUp(self): def setUp(self):
self.client = Client() # 测试初始化设置
self.factory = RequestFactory() self.client = Client() # 创建测试客户端
self.factory = RequestFactory() # 创建请求工厂
def test_validate_article(self): def test_validate_article(self):
# 测试文章验证功能
site = get_current_site().domain site = get_current_site().domain
user = BlogUser.objects.get_or_create( user = BlogUser.objects.get_or_create(
email="liangliangyy@gmail.com", email="liangliangyy@gmail.com",
username="liangliangyy")[0] username="liangliangyy")[0] # 获取或创建测试用户
user.set_password("liangliangyy") user.set_password("liangliangyy") # 设置密码
user.is_staff = True user.is_staff = True # 设置为员工
user.is_superuser = True user.is_superuser = True # 设置为超级用户
user.save() user.save()
response = self.client.get(user.get_absolute_url()) response = self.client.get(user.get_absolute_url()) # 访问用户详情页
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200) # 验证访问成功
response = self.client.get('/admin/servermanager/emailsendlog/') response = self.client.get('/admin/servermanager/emailsendlog/') # 访问邮件发送日志管理页
response = self.client.get('admin/admin/logentry/') response = self.client.get('admin/admin/logentry/') # 访问日志条目管理页
s = SideBar() s = SideBar() # 创建侧边栏
s.sequence = 1 s.sequence = 1 # 设置排序
s.name = 'test' s.name = 'test' # 设置名称
s.content = 'test content' s.content = 'test content' # 设置内容
s.is_enable = True s.is_enable = True # 启用侧边栏
s.save() s.save()
category = Category() category = Category() # 创建分类
category.name = "category" category.name = "category" # 设置分类名称
category.creation_time = timezone.now() category.creation_time = timezone.now() # 设置创建时间
category.last_mod_time = timezone.now() category.last_mod_time = timezone.now() # 设置修改时间
category.save() category.save()
tag = Tag() tag = Tag() # 创建标签
tag.name = "nicetag" tag.name = "nicetag" # 设置标签名称
tag.save() tag.save()
article = Article() article = Article() # 创建文章
article.title = "nicetitle" article.title = "nicetitle" # 设置文章标题
article.body = "nicecontent" article.body = "nicecontent" # 设置文章内容
article.author = user article.author = user # 设置作者
article.category = category article.category = category # 设置分类
article.type = 'a' article.type = 'a' # 设置类型为文章
article.status = 'p' article.status = 'p' # 设置状态为已发布
article.save() article.save()
self.assertEqual(0, article.tags.count()) self.assertEqual(0, article.tags.count()) # 验证初始标签数为0
article.tags.add(tag) article.tags.add(tag) # 添加标签
article.save() article.save()
self.assertEqual(1, article.tags.count()) self.assertEqual(1, article.tags.count()) # 验证标签数变为1
for i in range(20): for i in range(20):
# 批量创建20篇文章
article = Article() article = Article()
article.title = "nicetitle" + str(i) article.title = "nicetitle" + str(i)
article.body = "nicetitle" + str(i) article.body = "nicetitle" + str(i)
@ -81,152 +84,156 @@ class ArticleTest(TestCase):
article.save() article.save()
from blog.documents import ELASTICSEARCH_ENABLED from blog.documents import ELASTICSEARCH_ENABLED
if ELASTICSEARCH_ENABLED: if ELASTICSEARCH_ENABLED:
call_command("build_index") call_command("build_index") # 构建搜索索引
response = self.client.get('/search', {'q': 'nicetitle'}) response = self.client.get('/search', {'q': 'nicetitle'}) # 搜索文章
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200) # 验证搜索成功
response = self.client.get(article.get_absolute_url()) response = self.client.get(article.get_absolute_url()) # 访问文章详情页
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200) # 验证访问成功
from djangoblog.spider_notify import SpiderNotify from djangoblog.spider_notify import SpiderNotify
SpiderNotify.notify(article.get_absolute_url()) SpiderNotify.notify(article.get_absolute_url()) # 通知搜索引擎
response = self.client.get(tag.get_absolute_url()) response = self.client.get(tag.get_absolute_url()) # 访问标签详情页
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200) # 验证访问成功
response = self.client.get(category.get_absolute_url()) response = self.client.get(category.get_absolute_url()) # 访问分类详情页
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200) # 验证访问成功
response = self.client.get('/search', {'q': 'django'}) response = self.client.get('/search', {'q': 'django'}) # 搜索django相关内容
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200) # 验证搜索成功
s = load_articletags(article) s = load_articletags(article) # 加载文章标签
self.assertIsNotNone(s) self.assertIsNotNone(s) # 验证标签加载成功
self.client.login(username='liangliangyy', password='liangliangyy') self.client.login(username='liangliangyy', password='liangliangyy') # 登录用户
response = self.client.get(reverse('blog:archives')) response = self.client.get(reverse('blog:archives')) # 访问文章归档页
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200) # 验证访问成功
p = Paginator(Article.objects.all(), settings.PAGINATE_BY) p = Paginator(Article.objects.all(), settings.PAGINATE_BY) # 创建所有文章的分页器
self.check_pagination(p, '', '') self.check_pagination(p, '', '') # 测试分页功能
p = Paginator(Article.objects.filter(tags=tag), settings.PAGINATE_BY) p = Paginator(Article.objects.filter(tags=tag), settings.PAGINATE_BY) # 创建标签文章的分页器
self.check_pagination(p, '分类标签归档', tag.slug) self.check_pagination(p, '分类标签归档', tag.slug) # 测试标签分页
p = Paginator( p = Paginator(
Article.objects.filter( Article.objects.filter(
author__username='liangliangyy'), settings.PAGINATE_BY) author__username='liangliangyy'), settings.PAGINATE_BY) # 创建作者文章的分页器
self.check_pagination(p, '作者文章归档', 'liangliangyy') self.check_pagination(p, '作者文章归档', 'liangliangyy') # 测试作者分页
p = Paginator(Article.objects.filter(category=category), settings.PAGINATE_BY) p = Paginator(Article.objects.filter(category=category), settings.PAGINATE_BY) # 创建分类文章的分页器
self.check_pagination(p, '分类目录归档', category.slug) self.check_pagination(p, '分类目录归档', category.slug) # 测试分类分页
f = BlogSearchForm() f = BlogSearchForm() # 创建搜索表单
f.search() f.search() # 执行搜索
# self.client.login(username='liangliangyy', password='liangliangyy') # self.client.login(username='liangliangyy', password='liangliangyy')
from djangoblog.spider_notify import SpiderNotify 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 from blog.templatetags.blog_tags import gravatar_url, gravatar
u = gravatar_url('liangliangyy@gmail.com') u = gravatar_url('liangliangyy@gmail.com') # 获取头像URL
u = gravatar('liangliangyy@gmail.com') u = gravatar('liangliangyy@gmail.com') # 获取头像HTML
link = Links( link = Links( # 创建友情链接
sequence=1, sequence=1,
name="lylinux", name="lylinux",
link='https://wwww.lylinux.net') link='https://wwww.lylinux.net')
link.save() link.save()
response = self.client.get('/links.html') response = self.client.get('/links.html') # 访问友情链接页
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200) # 验证访问成功
response = self.client.get('/feed/') response = self.client.get('/feed/') # 访问RSS订阅
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200) # 验证访问成功
response = self.client.get('/sitemap.xml') response = self.client.get('/sitemap.xml') # 访问网站地图
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200) # 验证访问成功
self.client.get("/admin/blog/article/1/delete/") self.client.get("/admin/blog/article/1/delete/") # 访问文章删除页
self.client.get('/admin/servermanager/emailsendlog/') self.client.get('/admin/servermanager/emailsendlog/') # 访问邮件发送日志管理页
self.client.get('/admin/admin/logentry/') self.client.get('/admin/admin/logentry/') # 访问日志条目管理页
self.client.get('/admin/admin/logentry/1/change/') self.client.get('/admin/admin/logentry/1/change/') # 访问日志条目编辑页
def check_pagination(self, p, type, value): 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) for page in range(1, p.num_pages + 1): # 遍历所有分页
self.assertIsNotNone(s) s = load_pagination_info(p.page(page), type, value) # 加载分页信息
if s['previous_url']: self.assertIsNotNone(s) # 验证分页信息不为空
response = self.client.get(s['previous_url']) if s['previous_url']: # 如果有上一页链接
self.assertEqual(response.status_code, 200) response = self.client.get(s['previous_url']) # 访问上一页
if s['next_url']: self.assertEqual(response.status_code, 200) # 验证访问成功
response = self.client.get(s['next_url']) if s['next_url']: # 如果有下一页链接
self.assertEqual(response.status_code, 200) response = self.client.get(s['next_url']) # 访问下一页
self.assertEqual(response.status_code, 200) # 验证访问成功
def test_image(self): def test_image(self):
# 测试图片上传功能
import requests import requests
rsp = requests.get( rsp = requests.get(
'https://www.python.org/static/img/python-logo.png') 'https://www.python.org/static/img/python-logo.png') # 下载测试图片
imagepath = os.path.join(settings.BASE_DIR, 'python.png') imagepath = os.path.join(settings.BASE_DIR, 'python.png') # 图片保存路径
with open(imagepath, 'wb') as file: with open(imagepath, 'wb') as file:
file.write(rsp.content) file.write(rsp.content) # 保存图片
rsp = self.client.post('/upload') rsp = self.client.post('/upload') # 尝试上传图片(无签名)
self.assertEqual(rsp.status_code, 403) self.assertEqual(rsp.status_code, 403) # 验证被拒绝(需要签名)
sign = get_sha256(get_sha256(settings.SECRET_KEY)) sign = get_sha256(get_sha256(settings.SECRET_KEY)) # 生成上传签名
with open(imagepath, 'rb') as file: with open(imagepath, 'rb') as file:
imgfile = SimpleUploadedFile( imgfile = SimpleUploadedFile(
'python.png', file.read(), content_type='image/jpg') 'python.png', file.read(), content_type='image/jpg') # 创建上传文件对象
form_data = {'python.png': imgfile} form_data = {'python.png': imgfile}
rsp = self.client.post( rsp = self.client.post(
'/upload?sign=' + sign, form_data, follow=True) '/upload?sign=' + sign, form_data, follow=True) # 带签名上传图片
self.assertEqual(rsp.status_code, 200) self.assertEqual(rsp.status_code, 200) # 验证上传成功
os.remove(imagepath) os.remove(imagepath) # 删除临时图片文件
from djangoblog.utils import save_user_avatar, send_email 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( 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): 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): def test_commands(self):
# 测试管理命令
user = BlogUser.objects.get_or_create( user = BlogUser.objects.get_or_create(
email="liangliangyy@gmail.com", email="liangliangyy@gmail.com",
username="liangliangyy")[0] username="liangliangyy")[0] # 获取或创建测试用户
user.set_password("liangliangyy") user.set_password("liangliangyy") # 设置密码
user.is_staff = True user.is_staff = True # 设置为员工
user.is_superuser = True user.is_superuser = True # 设置为超级用户
user.save() user.save()
c = OAuthConfig() c = OAuthConfig() # 创建OAuth配置
c.type = 'qq' c.type = 'qq' # 设置类型为QQ
c.appkey = 'appkey' c.appkey = 'appkey' # 设置应用密钥
c.appsecret = 'appsecret' c.appsecret = 'appsecret' # 设置应用密钥
c.save() c.save()
u = OAuthUser() u = OAuthUser() # 创建OAuth用户
u.type = 'qq' u.type = 'qq' # 设置类型为QQ
u.openid = 'openid' u.openid = 'openid' # 设置OpenID
u.user = user u.user = user # 关联用户
u.picture = static("/blog/img/avatar.png") u.picture = static("/blog/img/avatar.png") # 设置头像
u.metadata = ''' u.metadata = '''
{ {
"figureurl": "https://qzapp.qlogo.cn/qzapp/101513904/C740E30B4113EAA80E0D9918ABC78E82/30" "figureurl": "https://qzapp.qlogo.cn/qzapp/101513904/C740E30B4113EAA80E0D9918ABC78E82/30"
}''' }''' # 设置元数据
u.save() u.save()
u = OAuthUser() u = OAuthUser() # 创建另一个OAuth用户
u.type = 'qq' u.type = 'qq' # 设置类型为QQ
u.openid = 'openid1' u.openid = 'openid1' # 设置不同的OpenID
u.picture = 'https://qzapp.qlogo.cn/qzapp/101513904/C740E30B4113EAA80E0D9918ABC78E82/30' u.picture = 'https://qzapp.qlogo.cn/qzapp/101513904/C740E30B4113EAA80E0D9918ABC78E82/30' # 设置网络头像
u.metadata = ''' u.metadata = '''
{ {
"figureurl": "https://qzapp.qlogo.cn/qzapp/101513904/C740E30B4113EAA80E0D9918ABC78E82/30" "figureurl": "https://qzapp.qlogo.cn/qzapp/101513904/C740E30B4113EAA80E0D9918ABC78E82/30"
}''' }''' # 设置元数据
u.save() u.save()
from blog.documents import ELASTICSEARCH_ENABLED from blog.documents import ELASTICSEARCH_ENABLED
if ELASTICSEARCH_ENABLED: if ELASTICSEARCH_ENABLED:
call_command("build_index") call_command("build_index") # 构建搜索索引
call_command("ping_baidu", "all") call_command("ping_baidu", "all") # 通知百度搜索引擎
call_command("create_testdata") call_command("create_testdata") # 创建测试数据
call_command("clear_cache") call_command("clear_cache") # 清除缓存
call_command("sync_user_avatar") call_command("sync_user_avatar") # 同步用户头像
call_command("build_search_words") call_command("build_search_words") # 构建搜索词

@ -3,60 +3,74 @@ from django.views.decorators.cache import cache_page
from . import views from . import views
app_name = "blog" app_name = "blog" # 定义应用的命名空间
urlpatterns = [ urlpatterns = [
# 首页URL
path( path(
r'', r'',
views.IndexView.as_view(), views.IndexView.as_view(),
name='index'), name='index'),
# 首页分页URL
path( path(
r'page/<int:page>/', r'page/<int:page>/',
views.IndexView.as_view(), views.IndexView.as_view(),
name='index_page'), name='index_page'),
# 文章详情页URL按ID和时间
path( path(
r'article/<int:year>/<int:month>/<int:day>/<int:article_id>.html', r'article/<int:year>/<int:month>/<int:day>/<int:article_id>.html',
views.ArticleDetailView.as_view(), views.ArticleDetailView.as_view(),
name='detailbyid'), name='detailbyid'),
# 分类详情页URL
path( path(
r'category/<slug:category_name>.html', r'category/<slug:category_name>.html',
views.CategoryDetailView.as_view(), views.CategoryDetailView.as_view(),
name='category_detail'), name='category_detail'),
# 分类详情分页URL
path( path(
r'category/<slug:category_name>/<int:page>.html', r'category/<slug:category_name>/<int:page>.html',
views.CategoryDetailView.as_view(), views.CategoryDetailView.as_view(),
name='category_detail_page'), name='category_detail_page'),
# 作者详情页URL
path( path(
r'author/<author_name>.html', r'author/<author_name>.html',
views.AuthorDetailView.as_view(), views.AuthorDetailView.as_view(),
name='author_detail'), name='author_detail'),
# 作者详情分页URL
path( path(
r'author/<author_name>/<int:page>.html', r'author/<author_name>/<int:page>.html',
views.AuthorDetailView.as_view(), views.AuthorDetailView.as_view(),
name='author_detail_page'), name='author_detail_page'),
# 标签详情页URL
path( path(
r'tag/<slug:tag_name>.html', r'tag/<slug:tag_name>.html',
views.TagDetailView.as_view(), views.TagDetailView.as_view(),
name='tag_detail'), name='tag_detail'),
# 标签详情分页URL
path( path(
r'tag/<slug:tag_name>/<int:page>.html', r'tag/<slug:tag_name>/<int:page>.html',
views.TagDetailView.as_view(), views.TagDetailView.as_view(),
name='tag_detail_page'), name='tag_detail_page'),
# 文章归档页URL缓存1小时
path( path(
'archives.html', 'archives.html',
cache_page( cache_page(
60 * 60)( 60 * 60)(
views.ArchivesView.as_view()), views.ArchivesView.as_view()),
name='archives'), name='archives'),
# 友情链接页URL
path( path(
'links.html', 'links.html',
views.LinkListView.as_view(), views.LinkListView.as_view(),
name='links'), name='links'),
# 文件上传URL
path( path(
r'upload', r'upload',
views.fileupload, views.fileupload,
name='upload'), name='upload'),
# 缓存清理URL
path( path(
r'clean', r'clean',
views.clean_cache_view, views.clean_cache_view,
name='clean'), name='clean'),
] ]

@ -25,39 +25,39 @@ logger = logging.getLogger(__name__)
class ArticleListView(ListView): 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 = '' page_type = ''
paginate_by = settings.PAGINATE_BY paginate_by = settings.PAGINATE_BY # 分页大小
page_kwarg = 'page' page_kwarg = 'page' # 页码参数名
link_type = LinkShowType.L link_type = LinkShowType.L # 链接显示类型
def get_view_cache_key(self): def get_view_cache_key(self):
return self.request.get['pages'] return self.request.get['pages']
@property @property
def page_number(self): def page_number(self):
# 获取当前页码
page_kwarg = self.page_kwarg page_kwarg = self.page_kwarg
page = self.kwargs.get( 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 return page
def get_queryset_cache_key(self): def get_queryset_cache_key(self):
""" """
子类重写.获得queryset的缓存key 子类重写.获得queryset的缓存key
""" """
raise NotImplementedError() raise NotImplementedError() # 子类必须实现此方法
def get_queryset_data(self): def get_queryset_data(self):
""" """
子类重写.获取queryset的数据 子类重写.获取queryset的数据
""" """
raise NotImplementedError() raise NotImplementedError() # 子类必须实现此方法
def get_queryset_from_cache(self, cache_key): def get_queryset_from_cache(self, cache_key):
''' '''
@ -65,14 +65,14 @@ class ArticleListView(ListView):
:param cache_key: 缓存key :param cache_key: 缓存key
:return: :return:
''' '''
value = cache.get(cache_key) value = cache.get(cache_key) # 尝试从缓存获取数据
if value: 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 return value
else: else:
article_list = self.get_queryset_data() article_list = self.get_queryset_data() # 获取数据
cache.set(cache_key, article_list) cache.set(cache_key, article_list) # 设置缓存
logger.info('set view cache.key:{key}'.format(key=cache_key)) logger.info('set view cache.key:{key}'.format(key=cache_key)) # 记录缓存设置日志
return article_list return article_list
def get_queryset(self): def get_queryset(self):
@ -80,296 +80,315 @@ class ArticleListView(ListView):
重写默认从缓存获取数据 重写默认从缓存获取数据
:return: :return:
''' '''
key = self.get_queryset_cache_key() key = self.get_queryset_cache_key() # 获取缓存键
value = self.get_queryset_from_cache(key) value = self.get_queryset_from_cache(key) # 从缓存获取数据
return value return value
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
kwargs['linktype'] = self.link_type kwargs['linktype'] = self.link_type # 添加链接类型到上下文
return super(ArticleListView, self).get_context_data(**kwargs) return super(ArticleListView, self).get_context_data(**kwargs) # 调用父类方法
class IndexView(ArticleListView): class IndexView(ArticleListView):
''' '''
首页 首页视图
''' '''
# 友情链接类型 link_type = LinkShowType.I # 首页链接类型
link_type = LinkShowType.I
def get_queryset_data(self): 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 return article_list
def get_queryset_cache_key(self): 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 return cache_key
class ArticleDetailView(DetailView): class ArticleDetailView(DetailView):
''' '''
文章详情页面 文章详情页面视图
''' '''
template_name = 'blog/article_detail.html' template_name = 'blog/article_detail.html' # 文章详情模板
model = Article model = Article # 关联的模型
pk_url_kwarg = 'article_id' pk_url_kwarg = 'article_id' # URL中的主键参数名
context_object_name = "article" context_object_name = "article" # 上下文变量名
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
comment_form = CommentForm() # 获取文章详情页的上下文数据
comment_form = CommentForm() # 评论表单
article_comments = self.object.comment_list()
parent_comments = article_comments.filter(parent_comment=None) article_comments = self.object.comment_list() # 获取文章评论列表
blog_setting = get_blog_setting() parent_comments = article_comments.filter(parent_comment=None) # 获取父级评论
paginator = Paginator(parent_comments, blog_setting.article_comment_count) blog_setting = get_blog_setting() # 获取博客设置
page = self.request.GET.get('comment_page', '1') paginator = Paginator(parent_comments, blog_setting.article_comment_count) # 评论分页器
if not page.isnumeric(): page = self.request.GET.get('comment_page', '1') # 获取评论页码
if not page.isnumeric(): # 验证页码是否为数字
page = 1 page = 1
else: else:
page = int(page) page = int(page)
if page < 1: if page < 1: # 页码不能小于1
page = 1 page = 1
if page > paginator.num_pages: if page > paginator.num_pages: # 页码不能大于总页数
page = paginator.num_pages page = paginator.num_pages
p_comments = paginator.page(page) p_comments = paginator.page(page) # 获取当前页评论
next_page = p_comments.next_page_number() if p_comments.has_next() else None 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 prev_page = p_comments.previous_page_number() if p_comments.has_previous() else None # 上一页页码
if next_page: if next_page: # 如果有下一页
kwargs[ kwargs[
'comment_next_page_url'] = self.object.get_absolute_url() + f'?comment_page={next_page}#commentlist-container' 'comment_next_page_url'] = self.object.get_absolute_url() + f'?comment_page={next_page}#commentlist-container' # 构建下一页URL
if prev_page: if prev_page: # 如果有上一页
kwargs[ kwargs[
'comment_prev_page_url'] = self.object.get_absolute_url() + f'?comment_page={prev_page}#commentlist-container' 'comment_prev_page_url'] = self.object.get_absolute_url() + f'?comment_page={prev_page}#commentlist-container' # 构建上一页URL
kwargs['form'] = comment_form kwargs['form'] = comment_form # 评论表单
kwargs['article_comments'] = article_comments kwargs['article_comments'] = article_comments # 所有评论
kwargs['p_comments'] = p_comments kwargs['p_comments'] = p_comments # 当前页评论
kwargs['comment_count'] = len( 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['next_article'] = self.object.next_article # 下一篇文章
kwargs['prev_article'] = self.object.prev_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 article = self.object
# Action Hook, 通知插件"文章详情已获取" # 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 return context
class CategoryDetailView(ArticleListView): class CategoryDetailView(ArticleListView):
''' '''
分类目录列表 分类目录列表视图
''' '''
page_type = "分类目录归档" page_type = "分类目录归档" # 页面类型
def get_queryset_data(self): 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 categoryname = category.name # 分类名称
self.categoryname = categoryname self.categoryname = categoryname # 保存分类名称
categorynames = list( 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( article_list = Article.objects.filter(
category__name__in=categorynames, status='p') category__name__in=categorynames, status='p') # 过滤分类下的文章
return article_list return article_list
def get_queryset_cache_key(self): def get_queryset_cache_key(self):
slug = self.kwargs['category_name'] # 生成分类目录缓存键
category = get_object_or_404(Category, slug=slug) slug = self.kwargs['category_name'] # 分类slug
categoryname = category.name category = get_object_or_404(Category, slug=slug) # 分类对象
self.categoryname = categoryname categoryname = category.name # 分类名称
self.categoryname = categoryname # 保存分类名称
cache_key = 'category_list_{categoryname}_{page}'.format( cache_key = 'category_list_{categoryname}_{page}'.format(
categoryname=categoryname, page=self.page_number) categoryname=categoryname, page=self.page_number) # 包含分类名和页码的缓存键
return cache_key return cache_key
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
# 获取分类目录页的上下文数据
categoryname = self.categoryname categoryname = self.categoryname
try: try:
categoryname = categoryname.split('/')[-1] categoryname = categoryname.split('/')[-1] # 提取分类名称的最后部分
except BaseException: except BaseException:
pass pass
kwargs['page_type'] = CategoryDetailView.page_type kwargs['page_type'] = CategoryDetailView.page_type # 页面类型
kwargs['tag_name'] = categoryname kwargs['tag_name'] = categoryname # 分类名称
return super(CategoryDetailView, self).get_context_data(**kwargs) return super(CategoryDetailView, self).get_context_data(**kwargs) # 调用父类方法
class AuthorDetailView(ArticleListView): class AuthorDetailView(ArticleListView):
''' '''
作者详情页 作者详情页视图
''' '''
page_type = '作者文章归档' page_type = '作者文章归档' # 页面类型
def get_queryset_cache_key(self): def get_queryset_cache_key(self):
# 生成作者详情页缓存键
from uuslug import slugify 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( 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 return cache_key
def get_queryset_data(self): def get_queryset_data(self):
author_name = self.kwargs['author_name'] # 获取作者文章数据
author_name = self.kwargs['author_name'] # 作者名称
article_list = Article.objects.filter( article_list = Article.objects.filter(
author__username=author_name, type='a', status='p') author__username=author_name, type='a', status='p') # 过滤作者文章
return article_list return article_list
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
author_name = self.kwargs['author_name'] # 获取作者详情页的上下文数据
kwargs['page_type'] = AuthorDetailView.page_type author_name = self.kwargs['author_name'] # 作者名称
kwargs['tag_name'] = author_name kwargs['page_type'] = AuthorDetailView.page_type # 页面类型
return super(AuthorDetailView, self).get_context_data(**kwargs) kwargs['tag_name'] = author_name # 作者名称
return super(AuthorDetailView, self).get_context_data(**kwargs) # 调用父类方法
class TagDetailView(ArticleListView): class TagDetailView(ArticleListView):
''' '''
标签列表页面 标签列表页面视图
''' '''
page_type = '分类标签归档' page_type = '分类标签归档' # 页面类型
def get_queryset_data(self): def get_queryset_data(self):
slug = self.kwargs['tag_name'] # 获取标签下的文章数据
tag = get_object_or_404(Tag, slug=slug) slug = self.kwargs['tag_name'] # 标签slug
tag_name = tag.name tag = get_object_or_404(Tag, slug=slug) # 标签对象
self.name = tag_name tag_name = tag.name # 标签名称
self.name = tag_name # 保存标签名称
article_list = Article.objects.filter( article_list = Article.objects.filter(
tags__name=tag_name, type='a', status='p') tags__name=tag_name, type='a', status='p') # 过滤标签文章
return article_list return article_list
def get_queryset_cache_key(self): def get_queryset_cache_key(self):
slug = self.kwargs['tag_name'] # 生成标签详情页缓存键
tag = get_object_or_404(Tag, slug=slug) slug = self.kwargs['tag_name'] # 标签slug
tag_name = tag.name tag = get_object_or_404(Tag, slug=slug) # 标签对象
self.name = tag_name tag_name = tag.name # 标签名称
self.name = tag_name # 保存标签名称
cache_key = 'tag_{tag_name}_{page}'.format( 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 return cache_key
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
# tag_name = self.kwargs['tag_name'] # 获取标签详情页的上下文数据
tag_name = self.name tag_name = self.name # 标签名称
kwargs['page_type'] = TagDetailView.page_type kwargs['page_type'] = TagDetailView.page_type # 页面类型
kwargs['tag_name'] = tag_name kwargs['tag_name'] = tag_name # 标签名称
return super(TagDetailView, self).get_context_data(**kwargs) return super(TagDetailView, self).get_context_data(**kwargs) # 调用父类方法
class ArchivesView(ArticleListView): class ArchivesView(ArticleListView):
''' '''
文章归档页面 文章归档页面视图
''' '''
page_type = '文章归档' page_type = '文章归档' # 页面类型
paginate_by = None paginate_by = None # 不分页
page_kwarg = None page_kwarg = None # 无页码参数
template_name = 'blog/article_archives.html' template_name = 'blog/article_archives.html' # 归档页面模板
def get_queryset_data(self): 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): def get_queryset_cache_key(self):
cache_key = 'archives' # 生成归档页面缓存键
cache_key = 'archives' # 固定缓存键
return cache_key return cache_key
class LinkListView(ListView): class LinkListView(ListView):
model = Links # 友情链接列表视图
template_name = 'blog/links_list.html' model = Links # 关联的模型
template_name = 'blog/links_list.html' # 模板名称
def get_queryset(self): def get_queryset(self):
return Links.objects.filter(is_enable=True) # 获取启用的友情链接
return Links.objects.filter(is_enable=True) # 过滤启用的链接
class EsSearchView(SearchView): class EsSearchView(SearchView):
# Elasticsearch搜索视图
def get_context(self): def get_context(self):
paginator, page = self.build_page() # 获取搜索结果的上下文数据
paginator, page = self.build_page() # 构建分页
context = { context = {
"query": self.query, "query": self.query, # 搜索查询
"form": self.form, "form": self.form, # 搜索表单
"page": page, "page": page, # 当前页
"paginator": paginator, "paginator": paginator, # 分页器
"suggestion": None, "suggestion": None, # 搜索建议
} }
if hasattr(self.results, "query") and self.results.query.backend.include_spelling: if hasattr(self.results, "query") and self.results.query.backend.include_spelling:
context["suggestion"] = self.results.query.get_spelling_suggestion() context["suggestion"] = self.results.query.get_spelling_suggestion() # 获取拼写建议
context.update(self.extra_context()) context.update(self.extra_context()) # 更新额外上下文
return context return context
@csrf_exempt @csrf_exempt # 免除CSRF保护
def fileupload(request): def fileupload(request):
""" """
该方法需自己写调用端来上传图片该方法仅提供图床功能 文件上传方法提供图床功能
:param request: :param request:
:return: :return:
""" """
if request.method == 'POST': if request.method == 'POST': # 只处理POST请求
sign = request.GET.get('sign', None) sign = request.GET.get('sign', None) # 获取签名
if not sign: if not sign: # 验证签名是否存在
return HttpResponseForbidden() return HttpResponseForbidden() # 无签名返回403
if not sign == get_sha256(get_sha256(settings.SECRET_KEY)): if not sign == get_sha256(get_sha256(settings.SECRET_KEY)): # 验证签名是否正确
return HttpResponseForbidden() return HttpResponseForbidden() # 签名错误返回403
response = [] response = [] # 响应列表
for filename in request.FILES: for filename in request.FILES: # 遍历所有上传的文件
timestr = timezone.now().strftime('%Y/%m/%d') timestr = timezone.now().strftime('%Y/%m/%d') # 时间目录
imgextensions = ['jpg', 'png', 'jpeg', 'bmp'] imgextensions = ['jpg', 'png', 'jpeg', 'bmp'] # 图片扩展名
fname = u''.join(str(filename)) fname = u''.join(str(filename)) # 文件名
isimage = len([i for i in imgextensions if fname.find(i) >= 0]) > 0 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) base_dir = os.path.join(settings.STATICFILES, "files" if not isimage else "image", timestr) # 基础目录
if not os.path.exists(base_dir): if not os.path.exists(base_dir): # 如果目录不存在
os.makedirs(base_dir) os.makedirs(base_dir) # 创建目录
savepath = os.path.normpath(os.path.join(base_dir, f"{uuid.uuid4().hex}{os.path.splitext(filename)[-1]}")) 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 not savepath.startswith(base_dir): # 路径安全检查
return HttpResponse("only for post") return HttpResponse("only for post")
with open(savepath, 'wb+') as wfile: with open(savepath, 'wb+') as wfile: # 写入文件
for chunk in request.FILES[filename].chunks(): for chunk in request.FILES[filename].chunks():
wfile.write(chunk) wfile.write(chunk)
if isimage: if isimage: # 如果是图片
from PIL import Image from PIL import Image
image = Image.open(savepath) image = Image.open(savepath) # 打开图片
image.save(savepath, quality=20, optimize=True) image.save(savepath, quality=20, optimize=True) # 压缩图片
url = static(savepath) url = static(savepath) # 生成静态文件URL
response.append(url) response.append(url) # 添加到响应列表
return HttpResponse(response) return HttpResponse(response) # 返回URL列表
else: else:
return HttpResponse("only for post") return HttpResponse("only for post") # 非POST请求返回提示
def page_not_found_view( def page_not_found_view(
request, request,
exception, exception,
template_name='blog/error_page.html'): template_name='blog/error_page.html'):
# 404错误页面视图
if exception: if exception:
logger.error(exception) logger.error(exception) # 记录错误日志
url = request.get_full_path() url = request.get_full_path() # 获取请求URL
return render(request, return render(request,
template_name, template_name,
{'message': _('Sorry, the page you requested is not found, please click the home page to see other?'), {'message': _('Sorry, the page you requested is not found, please click the home page to see other?'),
'statuscode': '404'}, 'statuscode': '404'}, # 错误信息
status=404) status=404) # 返回404状态码
def server_error_view(request, template_name='blog/error_page.html'): def server_error_view(request, template_name='blog/error_page.html'):
# 500错误页面视图
return render(request, return render(request,
template_name, template_name,
{'message': _('Sorry, the server is busy, please click the home page to see other?'), {'message': _('Sorry, the server is busy, please click the home page to see other?'),
'statuscode': '500'}, 'statuscode': '500'}, # 错误信息
status=500) status=500) # 返回500状态码
def permission_denied_view( def permission_denied_view(
request, request,
exception, exception,
template_name='blog/error_page.html'): template_name='blog/error_page.html'):
# 403错误页面视图
if exception: if exception:
logger.error(exception) logger.error(exception) # 记录错误日志
return render( return render(
request, template_name, { request, template_name, {
'message': _('Sorry, you do not have permission to access this page?'), '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): def clean_cache_view(request):
cache.clear() # 清理缓存视图
return HttpResponse('ok') cache.clear() # 清除所有缓存
return HttpResponse('ok') # 返回成功响应

@ -5,45 +5,50 @@ from django.utils.translation import gettext_lazy as _
def disable_commentstatus(modeladmin, request, queryset): 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): 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') disable_commentstatus.short_description = _('Disable comments') # 动作显示名称
enable_commentstatus.short_description = _('Enable comments') enable_commentstatus.short_description = _('Enable comments') # 动作显示名称
class CommentAdmin(admin.ModelAdmin): class CommentAdmin(admin.ModelAdmin):
list_per_page = 20 # 评论管理类
list_per_page = 20 # 每页显示20条记录
list_display = ( list_display = (
'id', 'id',
'body', 'body',
'link_to_userinfo', 'link_to_userinfo', # 自定义用户信息链接字段
'link_to_article', 'link_to_article', # 自定义文章链接字段
'is_enable', 'is_enable',
'creation_time') 'creation_time') # 列表页显示的字段
list_display_links = ('id', 'body', 'is_enable') list_display_links = ('id', 'body', 'is_enable') # 可点击链接的字段
list_filter = ('is_enable',) list_filter = ('is_enable',) # 右侧过滤器
exclude = ('creation_time', 'last_modify_time') exclude = ('creation_time', 'last_modify_time') # 排除的字段
actions = [disable_commentstatus, enable_commentstatus] actions = [disable_commentstatus, enable_commentstatus] # 管理员动作列表
raw_id_fields = ('author', 'article') raw_id_fields = ('author', 'article') # 使用原始ID字段外键搜索
search_fields = ('body',) search_fields = ('body',) # 搜索字段
def link_to_userinfo(self, obj): 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( return format_html(
u'<a href="%s">%s</a>' % u'<a href="%s">%s</a>' %
(link, obj.author.nickname if obj.author.nickname else obj.author.email)) (link, obj.author.nickname if obj.author.nickname else obj.author.email)) # 显示昵称或邮箱
def link_to_article(self, obj): def link_to_article(self, obj):
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( return format_html(
u'<a href="%s">%s</a>' % (link, obj.article.title)) u'<a href="%s">%s</a>' % (link, obj.article.title)) # 显示文章标题
link_to_userinfo.short_description = _('User') link_to_userinfo.short_description = _('User') # 字段显示名称
link_to_article.short_description = _('Article') link_to_article.short_description = _('Article') # 字段显示名称

@ -2,4 +2,4 @@ from django.apps import AppConfig
class CommentsConfig(AppConfig): class CommentsConfig(AppConfig):
name = 'comments' name = 'comments' # 指定应用的Python路径为'comments'

@ -5,9 +5,10 @@ from .models import Comment
class CommentForm(ModelForm): class CommentForm(ModelForm):
# 父评论ID字段使用隐藏输入框非必填
parent_comment_id = forms.IntegerField( parent_comment_id = forms.IntegerField(
widget=forms.HiddenInput, required=False) widget=forms.HiddenInput, required=False)
class Meta: class Meta:
model = Comment model = Comment # 指定模型为Comment
fields = ['body'] fields = ['body'] # 表单只包含body字段

@ -8,31 +8,32 @@ import django.utils.timezone
class Migration(migrations.Migration): class Migration(migrations.Migration):
initial = True initial = True # 标记为初始迁移
dependencies = [ dependencies = [
('blog', '0001_initial'), ('blog', '0001_initial'), # 依赖blog应用的初始迁移
migrations.swappable_dependency(settings.AUTH_USER_MODEL), migrations.swappable_dependency(settings.AUTH_USER_MODEL), # 依赖用户模型
] ]
operations = [ operations = [
# 创建评论表
migrations.CreateModel( migrations.CreateModel(
name='Comment', name='Comment',
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), # 自增主键
('body', models.TextField(max_length=300, verbose_name='正文')), ('body', models.TextField(max_length=300, verbose_name='正文')), # 评论正文最大长度300
('created_time', models.DateTimeField(default=django.utils.timezone.now, 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='修改时间')), ('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')), # 最后修改时间
('is_enable', models.BooleanField(default=True, 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='文章')), ('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='作者')), ('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='上级评论')), ('parent_comment', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='comments.comment', verbose_name='上级评论')), # 自关联上级评论,可为空
], ],
options={ options={
'verbose_name': '评论', 'verbose_name': '评论', # 单数显示名称
'verbose_name_plural': '评论', 'verbose_name_plural': '评论', # 复数显示名称
'ordering': ['-id'], 'ordering': ['-id'], # 按ID倒序排列
'get_latest_by': 'id', 'get_latest_by': 'id', # 指定获取最新记录的字段
}, },
), ),
] ]

@ -6,13 +6,14 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('comments', '0001_initial'), ('comments', '0001_initial'), # 依赖comments应用的初始迁移
] ]
operations = [ operations = [
# 修改Comment模型的is_enable字段默认值
migrations.AlterField( migrations.AlterField(
model_name='comment', model_name='comment',
name='is_enable', name='is_enable',
field=models.BooleanField(default=False, verbose_name='是否显示'), field=models.BooleanField(default=False, verbose_name='是否显示'), # 默认值从True改为False
), ),
] ]

@ -9,52 +9,61 @@ import django.utils.timezone
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL), migrations.swappable_dependency(settings.AUTH_USER_MODEL), # 依赖用户模型
('blog', '0005_alter_article_options_alter_category_options_and_more'), ('blog', '0005_alter_article_options_alter_category_options_and_more'), # 依赖blog应用的第五个迁移
('comments', '0002_alter_comment_is_enable'), ('comments', '0002_alter_comment_is_enable'), # 依赖comments应用的第二个迁移
] ]
operations = [ operations = [
# 修改Comment模型的元数据选项国际化
migrations.AlterModelOptions( migrations.AlterModelOptions(
name='comment', name='comment',
options={'get_latest_by': 'id', 'ordering': ['-id'], 'verbose_name': 'comment', 'verbose_name_plural': 'comment'}, options={'get_latest_by': 'id', 'ordering': ['-id'], 'verbose_name': 'comment', 'verbose_name_plural': 'comment'},
), ),
# 删除created_time字段
migrations.RemoveField( migrations.RemoveField(
model_name='comment', model_name='comment',
name='created_time', name='created_time',
), ),
# 删除last_mod_time字段
migrations.RemoveField( migrations.RemoveField(
model_name='comment', model_name='comment',
name='last_mod_time', name='last_mod_time',
), ),
# 添加creation_time字段
migrations.AddField( migrations.AddField(
model_name='comment', model_name='comment',
name='creation_time', name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'), field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
), ),
# 添加last_modify_time字段
migrations.AddField( migrations.AddField(
model_name='comment', model_name='comment',
name='last_modify_time', name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='last modify time'), field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='last modify time'),
), ),
# 修改article字段的verbose_name国际化
migrations.AlterField( migrations.AlterField(
model_name='comment', model_name='comment',
name='article', name='article',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.article', verbose_name='article'), field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.article', verbose_name='article'),
), ),
# 修改author字段的verbose_name国际化
migrations.AlterField( migrations.AlterField(
model_name='comment', model_name='comment',
name='author', name='author',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='author'), field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='author'),
), ),
# 修改is_enable字段的verbose_name国际化
migrations.AlterField( migrations.AlterField(
model_name='comment', model_name='comment',
name='is_enable', name='is_enable',
field=models.BooleanField(default=False, verbose_name='enable'), field=models.BooleanField(default=False, verbose_name='enable'),
), ),
# 修改parent_comment字段的verbose_name国际化
migrations.AlterField( migrations.AlterField(
model_name='comment', model_name='comment',
name='parent_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'), field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='comments.comment', verbose_name='parent comment'),
), ),
] ]

@ -9,31 +9,33 @@ from blog.models import Article
# Create your models here. # Create your models here.
class Comment(models.Model): class Comment(models.Model):
body = models.TextField('正文', max_length=300) # 评论模型
creation_time = models.DateTimeField(_('creation time'), default=now) body = models.TextField('正文', max_length=300) # 评论正文最大长度300字符
last_modify_time = models.DateTimeField(_('last modify time'), default=now) creation_time = models.DateTimeField(_('creation time'), default=now) # 创建时间
last_modify_time = models.DateTimeField(_('last modify time'), default=now) # 最后修改时间
author = models.ForeignKey( author = models.ForeignKey(
settings.AUTH_USER_MODEL, settings.AUTH_USER_MODEL,
verbose_name=_('author'), verbose_name=_('author'),
on_delete=models.CASCADE) on_delete=models.CASCADE) # 关联作者,级联删除
article = models.ForeignKey( article = models.ForeignKey(
Article, Article,
verbose_name=_('article'), verbose_name=_('article'),
on_delete=models.CASCADE) on_delete=models.CASCADE) # 关联文章,级联删除
parent_comment = models.ForeignKey( parent_comment = models.ForeignKey(
'self', 'self',
verbose_name=_('parent comment'), verbose_name=_('parent comment'),
blank=True, blank=True,
null=True, null=True,
on_delete=models.CASCADE) on_delete=models.CASCADE) # 自关联父级评论,可为空
is_enable = models.BooleanField(_('enable'), is_enable = models.BooleanField(_('enable'),
default=False, blank=False, null=False) default=False, blank=False, null=False) # 是否启用,默认禁用
class Meta: class Meta:
ordering = ['-id'] ordering = ['-id'] # 按ID倒序排列
verbose_name = _('comment') verbose_name = _('comment') # 单数显示名称
verbose_name_plural = verbose_name verbose_name_plural = verbose_name # 复数显示名称
get_latest_by = 'id' get_latest_by = 'id' # 指定获取最新记录的字段
def __str__(self): def __str__(self):
return self.body # 对象的字符串表示
return self.body # 返回评论正文

@ -12,68 +12,71 @@ from djangoblog.utils import get_max_articleid_commentid
class CommentsTest(TransactionTestCase): class CommentsTest(TransactionTestCase):
def setUp(self): def setUp(self):
self.client = Client() # 测试初始化设置
self.factory = RequestFactory() self.client = Client() # 创建测试客户端
self.factory = RequestFactory() # 创建请求工厂
from blog.models import BlogSettings from blog.models import BlogSettings
value = BlogSettings() value = BlogSettings() # 创建博客设置
value.comment_need_review = True value.comment_need_review = True # 设置评论需要审核
value.save() value.save()
self.user = BlogUser.objects.create_superuser( self.user = BlogUser.objects.create_superuser(
email="liangliangyy1@gmail.com", email="liangliangyy1@gmail.com",
username="liangliangyy1", username="liangliangyy1",
password="liangliangyy1") password="liangliangyy1") # 创建超级用户
def update_article_comment_status(self, article): def update_article_comment_status(self, article):
comments = article.comment_set.all() # 更新文章评论状态为启用
comments = article.comment_set.all() # 获取文章的所有评论
for comment in comments: for comment in comments:
comment.is_enable = True comment.is_enable = True # 启用评论
comment.save() comment.save()
def test_validate_comment(self): def test_validate_comment(self):
self.client.login(username='liangliangyy1', password='liangliangyy1') # 测试评论验证功能
self.client.login(username='liangliangyy1', password='liangliangyy1') # 登录用户
category = Category() category = Category() # 创建分类
category.name = "categoryccc" category.name = "categoryccc" # 设置分类名称
category.save() category.save()
article = Article() article = Article() # 创建文章
article.title = "nicetitleccc" article.title = "nicetitleccc" # 设置文章标题
article.body = "nicecontentccc" article.body = "nicecontentccc" # 设置文章内容
article.author = self.user article.author = self.user # 设置作者
article.category = category article.category = category # 设置分类
article.type = 'a' article.type = 'a' # 设置类型为文章
article.status = 'p' article.status = 'p' # 设置状态为已发布
article.save() article.save()
comment_url = reverse( comment_url = reverse(
'comments:postcomment', kwargs={ 'comments:postcomment', kwargs={
'article_id': article.id}) 'article_id': article.id}) # 生成评论提交URL
response = self.client.post(comment_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) article = Article.objects.get(pk=article.pk) # 重新获取文章
self.assertEqual(len(article.comment_list()), 0) self.assertEqual(len(article.comment_list()), 0) # 验证评论数为0需要审核
self.update_article_comment_status(article) self.update_article_comment_status(article) # 更新评论状态为启用
self.assertEqual(len(article.comment_list()), 1) self.assertEqual(len(article.comment_list()), 1) # 验证评论数变为1
response = self.client.post(comment_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) article = Article.objects.get(pk=article.pk) # 重新获取文章
self.update_article_comment_status(article) self.update_article_comment_status(article) # 更新评论状态为启用
self.assertEqual(len(article.comment_list()), 2) self.assertEqual(len(article.comment_list()), 2) # 验证评论数变为2
parent_comment_id = article.comment_list()[0].id parent_comment_id = article.comment_list()[0].id # 获取第一条评论的ID作为父评论
response = self.client.post(comment_url, response = self.client.post(comment_url,
{ {
@ -89,21 +92,21 @@ class CommentsTest(TransactionTestCase):
[ddd](http://www.baidu.com) [ddd](http://www.baidu.com)
''', ''', # 提交包含Markdown格式的评论
'parent_comment_id': parent_comment_id 'parent_comment_id': parent_comment_id # 设置父评论ID
}) })
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302) # 验证重定向响应
self.update_article_comment_status(article) self.update_article_comment_status(article) # 更新评论状态为启用
article = Article.objects.get(pk=article.pk) article = Article.objects.get(pk=article.pk) # 重新获取文章
self.assertEqual(len(article.comment_list()), 3) self.assertEqual(len(article.comment_list()), 3) # 验证评论数变为3
comment = Comment.objects.get(id=parent_comment_id) comment = Comment.objects.get(id=parent_comment_id) # 获取父评论
tree = parse_commenttree(article.comment_list(), comment) tree = parse_commenttree(article.comment_list(), comment) # 解析评论树
self.assertEqual(len(tree), 1) self.assertEqual(len(tree), 1) # 验证评论树长度
data = show_comment_item(comment, True) data = show_comment_item(comment, True) # 显示评论项
self.assertIsNotNone(data) self.assertIsNotNone(data) # 验证评论项数据不为空
s = get_max_articleid_commentid() s = get_max_articleid_commentid() # 获取最大文章ID和评论ID
self.assertIsNotNone(s) self.assertIsNotNone(s) # 验证返回值不为空
from comments.utils import send_comment_email from comments.utils import send_comment_email
send_comment_email(comment) send_comment_email(comment) # 测试发送评论邮件

@ -1,11 +1,11 @@
from django.urls import path
from . import views from . import views
app_name = "comments" app_name = "comments" # 定义应用的命名空间
urlpatterns = [ urlpatterns = [
# 文章评论提交URL
path( path(
'article/<int:article_id>/postcomment', 'article/<int:article_id>/postcomment', # URL模式文章ID/postcomment
views.CommentPostView.as_view(), views.CommentPostView.as_view(), # 关联评论提交视图
name='postcomment'), name='postcomment'), # URL名称
] ]

@ -9,20 +9,21 @@ logger = logging.getLogger(__name__)
def send_comment_email(comment): def send_comment_email(comment):
site = get_current_site().domain # 发送评论相关邮件
subject = _('Thanks for your comment') site = get_current_site().domain # 获取当前站点域名
article_url = f"https://{site}{comment.article.get_absolute_url()}" subject = _('Thanks for your comment') # 邮件主题
article_url = f"https://{site}{comment.article.get_absolute_url()}" # 构建文章完整URL
html_content = _("""<p>Thank you very much for your comments on this site</p> html_content = _("""<p>Thank you very much for your comments on this site</p>
You can visit <a href="%(article_url)s" rel="bookmark">%(article_title)s</a> You can visit <a href="%(article_url)s" rel="bookmark">%(article_title)s</a>
to review your comments, to review your comments,
Thank you again! Thank you again!
<br /> <br />
If the link above cannot be opened, please copy this link to your browser. If the link above cannot be opened, please copy this link to your browser.
%(article_url)s""") % {'article_url': article_url, 'article_title': comment.article.title} %(article_url)s""") % {'article_url': article_url, 'article_title': comment.article.title} # 感谢评论邮件内容
tomail = comment.author.email tomail = comment.author.email # 评论作者的邮箱
send_email([tomail], subject, html_content) send_email([tomail], subject, html_content) # 发送感谢评论邮件
try: try:
if comment.parent_comment: if comment.parent_comment: # 如果是对父评论的回复
html_content = _("""Your comment on <a href="%(article_url)s" rel="bookmark">%(article_title)s</a><br/> has html_content = _("""Your comment on <a href="%(article_url)s" rel="bookmark">%(article_title)s</a><br/> has
received a reply. <br/> %(comment_body)s received a reply. <br/> %(comment_body)s
<br/> <br/>
@ -31,8 +32,8 @@ def send_comment_email(comment):
If the link above cannot be opened, please copy this link to your browser. If the link above cannot be opened, please copy this link to your browser.
%(article_url)s %(article_url)s
""") % {'article_url': article_url, 'article_title': comment.article.title, """) % {'article_url': article_url, 'article_title': comment.article.title,
'comment_body': comment.parent_comment.body} 'comment_body': comment.parent_comment.body} # 评论回复通知邮件内容
tomail = comment.parent_comment.author.email tomail = comment.parent_comment.author.email # 父评论作者的邮箱
send_email([tomail], subject, html_content) send_email([tomail], subject, html_content) # 发送回复通知邮件
except Exception as e: except Exception as e:
logger.error(e) logger.error(e) # 记录邮件发送错误日志

@ -13,51 +13,55 @@ from .models import Comment
class CommentPostView(FormView): 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): 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): def get(self, request, *args, **kwargs):
article_id = self.kwargs['article_id'] # 处理GET请求重定向到文章详情页的评论区域
article = get_object_or_404(Article, pk=article_id) article_id = self.kwargs['article_id'] # 获取文章ID
url = article.get_absolute_url() article = get_object_or_404(Article, pk=article_id) # 获取文章对象
return HttpResponseRedirect(url + "#comments") url = article.get_absolute_url() # 获取文章绝对URL
return HttpResponseRedirect(url + "#comments") # 重定向到文章评论区域
def form_invalid(self, form): 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({ return self.render_to_response({
'form': form, 'form': form, # 返回包含错误信息的表单
'article': article 'article': article # 文章对象
}) })
def form_valid(self, form): def form_valid(self, form):
"""提交的数据验证合法后的逻辑""" """提交的数据验证合法后的逻辑"""
user = self.request.user user = self.request.user # 获取当前用户
author = BlogUser.objects.get(pk=user.pk) author = BlogUser.objects.get(pk=user.pk) # 获取博客用户对象
article_id = self.kwargs['article_id'] article_id = self.kwargs['article_id'] # 获取文章ID
article = get_object_or_404(Article, pk=article_id) article = get_object_or_404(Article, pk=article_id) # 获取文章对象
if article.comment_status == 'c' or article.status == 'c': if article.comment_status == 'c' or article.status == 'c': # 检查评论是否关闭
raise ValidationError("该文章评论已关闭.") raise ValidationError("该文章评论已关闭.") # 抛出验证错误
comment = form.save(False)
comment.article = article comment = form.save(False) # 创建评论对象但不保存到数据库
comment.article = article # 设置评论关联的文章
from djangoblog.utils import get_blog_setting from djangoblog.utils import get_blog_setting
settings = get_blog_setting() settings = get_blog_setting() # 获取博客设置
if not settings.comment_need_review: if not settings.comment_need_review: # 如果评论不需要审核
comment.is_enable = True comment.is_enable = True # 直接启用评论
comment.author = author comment.author = author # 设置评论作者
if form.cleaned_data['parent_comment_id']: if form.cleaned_data['parent_comment_id']: # 如果有父评论ID
parent_comment = Comment.objects.get( parent_comment = Comment.objects.get(
pk=form.cleaned_data['parent_comment_id']) pk=form.cleaned_data['parent_comment_id']) # 获取父评论对象
comment.parent_comment = parent_comment comment.parent_comment = parent_comment # 设置父评论
comment.save(True) comment.save(True) # 保存评论到数据库
return HttpResponseRedirect( return HttpResponseRedirect(
"%s#div-comment-%d" % "%s#div-comment-%d" %
(article.get_absolute_url(), comment.pk)) (article.get_absolute_url(), comment.pk)) # 重定向到新评论的位置
Loading…
Cancel
Save